Allow only 1 instance of each term in each custom taxonomy

Quite an interesting question. I cannot really find anything easy. Term creation is handled by the wp_insert_term() function. If you look at the source code, you’ll see that there really is not much filters or actions we can utilize before term insertion. pre_insert_term is about the only filter that happens before term validation and insertion.

By default, in any hierarchical taxonomy, you can have terms with duplicate names if those terms are not within the same hierarchy. There is no filter or action to alter this behavior, so this will be our challenge to stop this behavior and make sure that we can only have one instance of a term.

Non-hierarchical taxonomies, because there can only be top level terms, will not have this issue. There can not be any other term with the same name.

There is however one exception to the rules above, whenever we explicitly set an unique slug, we can have terms with the same name on the same level. We will however skip this all together and just focus solely on the term name supplied

So we really are left with only the pre_insert_term filter. Lets look at a possible solution

add_filter( 'pre_insert_term', function ( $term, $taxonomy )
{
    /**
     * Start by validating the term. The taxonomy is already validated
     * 
     * If the term is a numeric or int value, we will bail. We will let wp_insert_term
     * handle the process from here on. This is just a very loose check on our side
     */
    if ( is_numeric( $term ) )
        return $term;

    /**
     * $term is a valid non numeric string, so we most probably have a term name
     *
     * We will now basically use the same logic as `wp_insert_term` to validate the $term
     * name.
     */
    $term_name = strtolower( filter_var( $term, FILTER_SANITIZE_STRING ) );

    /**
     * Get all terms which matchings names like $term_name
     *
     * Getting terms by name is not an exact match, but a LIKE comparison, so
     * if you have names like 'Term A', 'term a' and 'term A', they all will match.
     * We will need to do an exact match later on
     */
    $name_matches = get_terms( 
        $taxonomy, 
        [
            'name'       => $term_name,
            'hide_empty' => false,
            'fields'     => 'names', // Only get term names
        ] 
    );
    // If $name_matches is empty, we do not have duplicates, bail
    if ( !$name_matches )
        return $term;

    // Convert all names into lowercase
    $names_array = array_map( 'strtolower', $name_matches );
    // Test to see if we have an exact term name match, if so, return WP_Error
    if ( in_array( $term_name, $names_array ) )
        return new WP_Error( 
            'term_exists',
            __( 'You cannot have a term with the same name, choose a unique name.' ),
            $term 
        );

    // OK we have a unique term name, let 'wp_insert_term' continue the rest of the process
    return $term;   
}, 10, 2 );

What we have done is to get all terms with names like the term we would like to insert, convert them all to lower case, and then test if our term name is in the array of term names. If so, we will return a WP_Error object in order to stop the whole term insertion process

Obviously, you do not need any other checks between taxonomies as you can have a term with the same name between different taxonomies.