Skip to main content
Skip to main content

Developers Doc

Search Engine: Query Building and Map Search

8 mins read 5 Views 3+

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 build WP_Query args 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 actionnoncepageper_page, and instance_id, plus the collected filters from window.havenlyticsAJAX.getFormData() or collectAllFilterValues().

Collected filters

min_pricemax_pricebedroomsbathroomsreception_roomsgaragesproperty_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), orderbyview_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 AbortController timeout per request and a debounced refresh.
  • Posts-per-page precedence in resolvePostsPerPagedata-posts-per-page → havenlyticsAJAX.postsPerPage → localized properties_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_propertypost_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_deptsproperty_type → hvnly_prop_typeslocation → hvnly_prop_locationsstatus → 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()

FilterMeta keyComparison
price range_hvnly_property_priceNUMERIC >= / <= (AND)
bedrooms_hvnly_property_bedroomsNUMERIC >=
bathrooms_hvnly_property_bathroomsNUMERIC >=
reception_rooms_hvnly_property_reception_roomsNUMERIC >=
garages_hvnly_property_garage_sqftNUMERIC >=
featuredget_featured_meta_query()_hvnly_property_featured OR _hvnly_property_action_tool_is_featured = 1
property_ids[]_hvnly_unique_property_idLIKE (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_oldestprice_low / price_high (meta_value_num on _hvnly_property_price), area (_hvnly_property_sqft), bedroomsviews (_hvnly_property_views), rating (_hvnly_property_rating), and rand. The default falls back to hvnly_get_default_sort_option().

Keyword, paging and view type

  • s is populated from address_keyword.
  • resolve_paged() gives an AJAX page precedence over a hidden paged field, and is aware of the Elementor hvnly_paged_{widget} param.
  • resolve_view_type() returns gridlist, or map.

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_propertieshvnly_load_morehvnly_get_filtershvnly_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_postsmax_pagescurrent_pageposts_per_pagepagination_htmlresults_count_htmlloaded_countposts_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:

  1. The builder map field, via hvnly_resolve_field_meta() with metaKey latitude / longitude / address.
  2. Pointer meta: _hvnly_active_map_latitude_key / _longitude_key / _address_key.
  3. Convention keys: {base}_latitude / _longitude / _preview / _address.
  4. Legacy keys: _hvnly_property_location_Latitude / Longitude, and _hvnly_property_latitude / longitude.
  5. 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_scanned and found_posts, but they are excluded from the markers. Expect posts_with_coordinates to be lower than found_posts.

Note: The card AJAX handlers (search_propertiesload_more_properties) render each result through hvnly_render_property_card() — the exact same renderer used for server-side rendering, so AJAX cards and SSR cards are always visually identical.

Query filters

FilterWhere it applies
hvnly_property_query_argsFinal args produced by PropertyQueryArgsBuilder.
hvnly_property_query_executor_argsArgs after the executor wraps the builder.
hvnly_elementor_load_more_query_argsArgs 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 PropertyQueryArgsBuilder or the hvnly_property_query_args filter so SSR, AJAX, and load-more stay in sync.
  • When adding meta conditions, append to meta_query rather than replacing it — the builder has already populated price/room clauses.
  • Store coordinates under the builder map field’s metaKey so the first (fastest) step of the fallback chain resolves them.
  • Respect the map’s coordinate-gating: surface posts_with_coordinates vs found_posts in any custom UI so users understand why some results have no marker.

Performance notes

  • PropertyQueryExecutor memoizes and caches results via PropertyQueryCacherand ordering deliberately bypasses the cache, so avoid random sort on high-traffic archives.
  • lazy_load_term_meta => true keeps 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_request nonce 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_Query args by hand and drifting out of sync with the SSOT builder.
  • Overwriting meta_query in 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.

Need more help?

Can't find what you're looking for? Our team and community are here to help you ship faster.