The Havenlytics search engine spans two layers: a frontend JavaScript map/search client, and a server-side query builder that is the single source of truth for every property query. This guide documents both — the AJAX contract, the collected filters, the PropertyQueryArgsBuilder, the executor and cache, the map handler’s response shape and coordinate-resolution fallback chain, and the filters you use to extend queries.
Note: Since version 3.1.0,
PropertyQueryArgsBuilder::build()is the SSOT for server-side rendering, AJAX, and load-more queries. Do not buildWP_Queryargs for properties by hand — go through the builder or its filters so every code path stays consistent.
Frontend: HavenlyticsPropertyMap
The client lives in assets/frontend/js/property-search/hvnly-frontend-property-map-search.js — an IIFE class, HavenlyticsPropertyMap. It drives the interactive map and result list.
AJAX actions and request params
It posts two actions — hvnly_get_properties_for_map and hvnly_render_map_container — to window.hvnly_PROPERTY_ajax.ajax_url (falling back to hvnlyFrontend.ajax_url, then ajaxurl). Each request (fetchSinglePage) sends action, nonce, page, per_page, and instance_id, plus the collected filters from window.havenlyticsAJAX.getFormData() or collectAllFilterValues().
Collected filters
min_price, max_price, bedrooms, bathrooms, reception_rooms, garages, property_ids[], amenities[], and the taxonomy checkbox arrays hvnly_prop_types[], hvnly_prop_locations[], hvnly_prop_features[], hvnly_prop_reviews[], hvnly_prop_tags[], hvnly_prop_badges[], hvnly_prop_status[]. Also address_keyword (debounced 300ms), orderby, view_type, and department. The top dropdowns map property_type[] → hvnly_prop_types[] and location[] → hvnly_prop_locations[].
Client behaviour
- Cumulative loading with a per-page response cache.
- A 30-second
AbortControllertimeout per request and a debounced refresh. - Posts-per-page precedence in
resolvePostsPerPage:data-posts-per-page→havenlyticsAJAX.postsPerPage→ localizedproperties_per_page/per_page→12. - Elementor instance-id aware, so multiple widgets on a page don’t collide.
Map providers
Two providers are supported: Leaflet / OpenStreetMap (the default) and Google Maps. The config resolves openstreetmap → Leaflet, and google without an api_key also falls back to Leaflet (configured via window.hvnly_map_params). Marker colour resolves the CSS variable --hvnly-primary-color, defaulting to #6C60FE.
Example: AJAX request params
// Shape of a map-search request body (URL-encoded form data)
const params = new URLSearchParams();
params.set( 'action', 'hvnly_get_properties_for_map' );
params.set( 'nonce', window.hvnly_PROPERTY_ajax.nonce );
params.set( 'page', 1 );
params.set( 'per_page', 12 );
params.set( 'instance_id', instanceId );
params.set( 'min_price', 100000 );
params.set( 'max_price', 500000 );
params.set( 'bedrooms', 3 );
// taxonomy arrays use [] suffixes
params.append( 'hvnly_prop_types[]', 'apartment' );
fetch( window.hvnly_PROPERTY_ajax.ajax_url, {
method: 'POST',
body: params,
} );
Server-side: PropertyQueryArgsBuilder
includes/Frontend/Query/PropertyQueryArgsBuilder.php exposes build( $data, $page, $per_page, $options ). It starts from a base of post_type=hvnly_property, post_status=publish, with resolved paged and posts_per_page, then layers taxonomy, meta, and ordering clauses.
apply_tax_queries()
Maps incoming filters to taxonomy queries: department → hvnly_prop_depts, property_type → hvnly_prop_types, location → hvnly_prop_locations, status → hvnly_prop_status, plus generic slug taxonomies including amenities, and term-id params in_tag / in_badge / in_status / in_type / in_location / in_feature / in_review. When multiple taxonomies are present they are joined with relation => 'OR'.
apply_meta_queries()
| Filter | Meta key | Comparison |
|---|---|---|
| price range | _hvnly_property_price | NUMERIC >= / <= (AND) |
| bedrooms | _hvnly_property_bedrooms | NUMERIC >= |
| bathrooms | _hvnly_property_bathrooms | NUMERIC >= |
| reception_rooms | _hvnly_property_reception_rooms | NUMERIC >= |
| garages | _hvnly_property_garage_sqft | NUMERIC >= |
| featured | get_featured_meta_query() | _hvnly_property_featured OR _hvnly_property_action_tool_is_featured = 1 |
| property_ids[] | _hvnly_unique_property_id | LIKE (OR) |
The meta groups are joined with AND (the featured and property-ids sub-clauses use OR internally).
apply_orderby()
Supported sorts: title a-z / z-a, date / date_oldest, price_low / price_high (meta_value_num on _hvnly_property_price), area (_hvnly_property_sqft), bedrooms, views (_hvnly_property_views), rating (_hvnly_property_rating), and rand. The default falls back to hvnly_get_default_sort_option().
Keyword, paging and view type
sis populated fromaddress_keyword.resolve_paged()gives an AJAXpageprecedence over a hiddenpagedfield, and is aware of the Elementorhvnly_paged_{widget}param.resolve_view_type()returnsgrid,list, ormap.
Executor and cache
includes/Frontend/Query/PropertyQueryExecutor.php wraps the builder via query() / build_args(), adding memoization and a PropertyQueryCache layer (bypassed for rand ordering or when caching is off). It sets no_found_rows => false and lazy_load_term_meta => true. Supporting pieces are PropertyQueryCache.php and PropertyQueryManager.php (a singleton that isolates the query stack).
Map AJAX handler
includes/Frontend/AjaxHandler.php registers the search actions — hvnly_search_properties, hvnly_load_more, hvnly_get_filters, hvnly_get_properties_for_map, and hvnly_render_map_container — all with nopriv variants.
hvnly_get_properties_for_map() response
This handler verifies the hvnly_ajax_request nonce, then runs PropertyQueryExecutor::query( ..., ['filter_context' => 'map'] ). For each property it returns { id, title, lat, lng, address, price, bedrooms, bathrooms, thumbnail (medium), link, has_coordinates }. The envelope adds found_posts, max_pages, current_page, posts_per_page, pagination_html, results_count_html, loaded_count, posts_scanned, and posts_with_coordinates.
Coordinate resolution fallback chain
get_property_map_data() resolves a property’s coordinates through an ordered fallback chain, stopping at the first that yields valid values:
- The builder map field, via
hvnly_resolve_field_meta()withmetaKeylatitude/longitude/address. - Pointer meta:
_hvnly_active_map_latitude_key/_longitude_key/_address_key. - Convention keys:
{base}_latitude/_longitude/_preview/_address. - Legacy keys:
_hvnly_property_location_Latitude/Longitude, and_hvnly_property_latitude/longitude. scan_post_meta_for_coordinates()as a last resort.
Values are validated by normalize_map_coordinates(), which requires numeric coordinates.
Note: The map view is coordinate-gated. Properties without valid coordinates are still counted in
posts_scannedandfound_posts, but they are excluded from the markers. Expectposts_with_coordinatesto be lower thanfound_posts.
Note: The card AJAX handlers (
search_properties,load_more_properties) render each result throughhvnly_render_property_card()— the exact same renderer used for server-side rendering, so AJAX cards and SSR cards are always visually identical.
Query filters
| Filter | Where it applies |
|---|---|
hvnly_property_query_args | Final args produced by PropertyQueryArgsBuilder. |
hvnly_property_query_executor_args | Args after the executor wraps the builder. |
hvnly_elementor_load_more_query_args | Args for Elementor load-more queries. |
Example: add a custom meta_query
<?php
/**
* Restrict all property queries to listings flagged with a custom
* "_my_premium" meta value, without touching any core code path.
*/
add_filter( 'hvnly_property_query_args', function ( array $args ) {
if ( empty( $args['meta_query'] ) ) {
$args['meta_query'] = array();
}
// Preserve the builder's existing relation semantics.
$args['meta_query'][] = array(
'key' => '_my_premium',
'value' => '1',
'compare' => '=',
);
return $args;
} );
Best practices
- Always route custom property queries through
PropertyQueryArgsBuilderor thehvnly_property_query_argsfilter so SSR, AJAX, and load-more stay in sync. - When adding meta conditions, append to
meta_queryrather than replacing it — the builder has already populated price/room clauses. - Store coordinates under the builder map field’s
metaKeyso the first (fastest) step of the fallback chain resolves them. - Respect the map’s coordinate-gating: surface
posts_with_coordinatesvsfound_postsin any custom UI so users understand why some results have no marker.
Performance notes
PropertyQueryExecutormemoizes and caches results viaPropertyQueryCache;randordering deliberately bypasses the cache, so avoid random sort on high-traffic archives.lazy_load_term_meta => truekeeps term-meta hydration lazy — don’t force term-meta eager loads in your filters.- The client caches responses per page and aborts requests after 30 seconds; keep any server-side additions to the query fast to stay inside that window.
Security notes
- The map handler verifies the
hvnly_ajax_requestnonce before running; if you add your own AJAX entry points, verify a nonce the same way. property_ids[]is matched with a LIKE against_hvnly_unique_property_id— treat incoming filter values as untrusted and let the builder sanitize them; never interpolate raw filter input into SQL.
Common mistakes
- Building property
WP_Queryargs by hand and drifting out of sync with the SSOT builder. - Overwriting
meta_queryin a filter and wiping the builder’s price/bedroom clauses. - Expecting every found property to appear on the map — coordinate-less listings are excluded from markers by design.
- Assuming a dedicated search-builder class exists; the search engine is query-building code, not a drag-and-drop builder.