You two JOIN elements both with the same table. When you need to join one table more then once, you need to make each join unique by giving the table alias. When WP make the whole query, it takes that into account, but you have add your own code that doesn't do that.

So, first function:

    function cf_search_join( $join ) {
        global $wpdb;
    
        if ( is_search() ) {    
            $join .=' LEFT JOIN '.$wpdb->postmeta. ' cfmeta ON '. $wpdb->posts . '.ID = cfmeta.post_id ';
        }
    
        return $join;
    }
    add_filter('posts_join', 'cf_search_join' );

This sets postmeta table alias 'cfmeta' and uses it in the ON.

And the second one:

    function cf_search_where( $where ) {
        global $pagenow, $wpdb;
    
        if ( is_search() ) {
            $where = preg_replace(
                "/\(\s*".$wpdb->posts.".post_title\s+LIKE\s*(\'[^\']+\')\s*\)/",
                "(".$wpdb->posts.".post_title LIKE $1) OR (cfmeta.meta_value LIKE $1)", $where );
        }
    
        return $where;
    }
    add_filter( 'posts_where', 'cf_search_where' );

Using the 'cfmeta' instead of wp_postmeta.

Milan