Theory
Term association counts are stored statically in the database along with their respective terms. This means that there is no way to determine how many posts share two or more terms simply by examining the terms themselves.
I haven't tested or debugged this solution at this point - nor am I sure that it would be any more efficient than Howdy_McGee's answer - but I found the problem interesting and wanted to see where it took me.
The idea was to mitigate the expense of multiple post queries every page load by tracking changes to each property's features when a property is created/updated, then storing the IDs of every property associated with a feature in that feature's term metadata. Counting the items in the intersection of relevant features' property lists then yields the number of properties which share those features.
In principal, I would think that this method would be more efficient than running a post query for every term every page-load. But in practice I think it will choke harder when dealing with substantially large collections of features and/or properties.
(Preliminary) Implementation
Changes in a property's associated features can be tracked by comparing previous terms store in post metadata against the property's current terms. When a change is detected, the relevant feature terms' metadata lists of associated properties can then be updated:
add_action( 'save_post', 'wpse233841_update_property_feature_lists', 10, 2 );
function wpse233841_update_property_feature_lists( $post_id, $post ) {
if ( wp_is_post_revision( $post_id ) )
return;
if( 'property' !== $post->post_type )
return;
$previous_feature_ids = get_post_meta( $post_id, '_previous_features', true );
if( $previous_feature_ids )
$previous_feature_ids = maybe_unserialize( $previous_feature_ids );
else
$previous_feature_ids = [];
$feature_ids = wp_get_object_terms(
$post_id,
'features',
[
'fields' => 'ids'
]
);
sort( $feature_ids );
if( $previous_feature_ids == $feature_ids )
return;
$added = array_diff( $feature_ids, $previous_feature_ids );
$removed = array_diff( $previous_feature_ids, $feature_ids );
foreach( $added as $added_feature_id )
wpse233841_feature_add_property( $added_feature_id, $post_id );
foreach( $removed as $removed_feature_id )
wpse233841_feature_remove_property( $removed_feature_id, $post_id );
update_post_meta( $post_id, '_previous_features', $feature_ids );
}
function wpse233841_feature_add_property( $feature_id, $property_id ) {
$property_ids = get_term_meta( $feature_id, '_properties', true );
if( $property_ids )
$property_ids = maybe_unserialize( $property_ids );
else
$property_ids = [];
$property_ids[] = $property_id;
update_term_meta( $feature_id, '_properties', $property_ids );
}
function wpse233841_feature_remove_property( $feature_id, $property_id ) {
$property_ids = get_term_meta( $feature_id, '_properties', true );
if( ! $property_ids )
return;
$property_ids = maybe_unserialize( $property_ids );
$property_ids = array_diff( $property_ids, [$property_id] );
update_term_meta( $feature_id, '_properties', $property_ids );
}
(I can't remember if I'm handling my metadata correctly - It's been a while).
Now, every feature's associated properties list can be quickly obtained through term meta queries instead of querying posts. These lists could even be easily be packed into JSON and delivered to the browser, enabling instantaneous filtering (minus actually querying relevant properties). Property lists in-hand, getting a list of properties that have any given set of features becomes a matter of intersecting the relevant arrays:
function wpse233841_get_properties_by_features( $feature_slugs ) {
static $feature_properties = [];
if( empty( $feature_properties ) ) {
$all_feature_ids = get_terms([
'taxonomy' => 'features',
'fields' => 'id=>slug',
'hide_empty' => false
]);
foreach ($ all_feature_ids as $feature_id => $feature_slug ) {
$property_ids = get_term_meta( $feature_id, '_properties', true );
$feature_properties[ $feature_slug ] = maybe_unserialize( $property_ids );
}
}
return call_user_func_array(
'array_intersect',
array_intersect_key( $feature_properties, array_flip( $feature_slugs ) )
);
}
function wpse233841_get_feature_property_count( $feature_slug, $filter_features = [] ) {
if( !in_array( $feature_slug, $filter_features ) )
$filter_features[] = $feature_slug;
return count( wpse233841_get_properties_by_features( $feature_slugs ) );
}
In your template then, relevant property counts can be obtained by calling wpse233841_get_feature_property_count()
with a "subject" feature slug as the first argument, and an array of features to filter the count by as the second argument:
<?php
$all_features = get_terms('features');
/* Features in search query */
$required_features_slugs = array();
if( isset ( $_GET['features'] ) ) {
$required_features_slugs = $_GET['features'];
}
$features_count = count ($all_features);
if($features_count > 0):
?>
<div class="more-options-wrapper clearfix">
<?php
foreach ( $all_features as $feature ) {
$cb_atts = [
'id' => 'feature-' . $feature->slug,
'name' => 'features[]',
'value' => $feature->slug,
'onclick' => 'document.getElementById(\'refine-properties\').submit();'
];
$cb_atts_string = '';
foreach( $cb_atts as $key => $value )
$cb_atts_string .= ' ' . $key . '="' . $value . '"';
if( in_array( $feature->slug, $required_features_slugs ) )
$cb_atts_string .= ' checked ';
?>
<div class="option-bar">
<input type="checkbox" <?= $cb_atts_string ?> />
<label for="<?= $cb_atts['id'] ?>"><?= $feature->name ?> <small>(<?= wpse233841_get_feature_property_count( $feature->slug, $required_features_slugs ) ?>)</small>
</label>
</div>
<?php
}
?>
</div>
<?php
endif;
?>