ARTICLES

Filter JetEngine Map Listing By User Input Address & Radius / Distance (JetEngine, JetSmartFilters, Query Builder)

This tutorial will use JetSmartFilters to create an address search input and radius / distance dropdown for visitor to find nearby branches (Custom Post Type) in a map listing. The built-in JetEngine can only show the map listing in fixed query builder settings or user’s device geo location which cannot fulfill my client’s requirement.

You must follow this tutorial if trying to create a map listing like find best developers in certain location, find nearby coffee shops, find best X in Y location etc.

The final result will be like this.

Tutorial Environment Setup

  • WordPress 6.0.1
  • Elementor 3.7.2
  • Elementor Pro 3.7.3 (Optional, I just use it to style some widgets.)
  • JetEngine 3.0.3.1
  • JetSmartFilters 2.3.13
  • PHP 7.4
  • Open LiteSpeed Server
  • Google API for Geocoding provider (I prefer Google due to it’s accuracy of address search)
  • All custom codes in this tutorial place in theme / child theme functions.php

Plan / Expected Result

  1. A custom post type Branches to save all branches’ address and phone in the website.
  2. A map listing display the headquarter only branch with default 10KM distance around it. (Query builder)
  3. An address search input to allow visitor find nearby branches. (Not using device geo location)
  4. A dropdown to allow visitor set the search distance / radius in KM.
  5. Hide the map and show some text if the search result is empty.

Understand Basic Concept

  • You can choose different provider for map rendering and geocoding. In this tutorial, I will use leaflet for map rendering, and Google for geocoding. Make sure you tick the Separate Geocoding API key in Maps Settings.
  • For map rendering, your CPT only needs to provide latitude and longitude, the map provider will then populate the markers automatically.
  • In this tutorial, I just use address field and let the geocoding provider (Google) to extract it’s latitude and longitude automatically. Remember to enable Preload coordinates by address in Maps Settings, so JetEngine will store the latitude and longitude in a hidden custom field instead of keep calling API on each render.
  • For the search functionality, we will use code to let JetEngine use geocoding provider to transform visitor’s input address into latitude and longitude data, and do a SQL search (handled by JetEngine) from our CPT filter by distance / radius setting.
  • Example: Visitor input “Changi Air Port, Singapore”. We will first use Google to translate this address into latitude and longitude data (Data A). Then search from our CPT where their distance / radius within 10 KM from the Data A. If has result, JetEngine will erase current map markers and replace them with new results.
  • Please be noticed that the distance calculation in JetEngine is the radius in KM or Mile from point A to point B without considering the “travel possibility”.

Step 1: Custom Post Type

Very simple, I just add 2 fields for my Branches CPT, Address (text) and Phone (text). I don’t like to use the map field because it’s not user friendly, I don’t even able to enter the address to get latitude and longitude. You could create 2 separate fields to store latitude and longitude as well, this might save your API calls.

My Branches CPT only has 2 fields.
My Branches CPT only has 2 fields.

Once done, build your branches data.

Build my branches data
More data for the tutorial.

Tips: If you want to check your location’s latitude and longitude in Google Map, just right click and you will be able to copy them easily.

Right click on Google Map to easily get your location's latitude and longitude.
Right click on Google Map to easily get your location’s latitude and longitude.

Step 2: Query Builder

Make sure Map Listings activated in JetEngine Modules settings.

My Query Builder important settings:

  • Query Type: Posts Query (Branches is CPT)
  • General > Post Type: Branches
  • General > Post Status: Publish (So I can use draft to hide certain branches when needed)
  • Pagination > Posts Per Page: -1
  • Geo Search > Select location: On the map, I pin on my HQ branch location. Honestly, this step really sucks and I hope JetEngine will improve so I could just enter an address or coordinate instead of find my location on the map. (This setting instructs JetEngine set a WHERE lat = y, lng = x clause)
  • Geo Search > Address Field: address (Use your field name. Use comma to join your separated latitude and longitude field if you store the coordinates in individual field)
  • Geo Search > Distance: 10
  • Geo Search > Unites: Kilometers

After creation, turn on the Preview results and you should get some results. In my example, I only get 1 post because there is no other branch within 10KM based on my pinned location. You could play around with the Distance setting and the result should be changed accordingly.

Important – Ensure there is a geo_query_distance key in each post result. This represents the distance from your pinned location to the branch location. If you don’t see this, probably your Address Field configured wrongly.

Example: 0.0158KM distance from my pinned location to this result.
Example: 0.0158KM distance from my pinned location to this result.

Step 3: Listing Grid For Map Marker Popup

Heads to JetEngine > Listings > Add New. Please choose the query in Step 2 as Listing source.

Choose the correct query from Query Builder source.
Choose the correct query from Query Builder source.

Create a simple popup card design for each map marker (branch) to be used on Map Listing later. You should use Dynamic Field widget to show CPT’s data.

I just display the branch title, address and phone on my popup. Set some padding and style as you like.
I just display the branch title, address and phone on my popup. Set some padding and style as you like.

Step 4: Map Listing Widget

Create a page and insert the Map Listing widget. Assign a unique CSS ID for it so we can use JetSmartFilter later.

branch-map as my map listing CSS ID
branch-map as my map listing CSS ID

Important settings on Map Listing widget:

  • General > Listing: Choose the designed listing grid in Step 3.
  • General > Address Meta Field: address. Please choose Use Lat Lng Address Meta Field if you use lat lng field.
  • General > Automatically detect map center: Yes
  • Marker > Choose your desire icon, you can turn on Marker Clustering if you like.
  • Popup > Add popup preloader: Yes (For better UX)
  • Custom Query > Use Custom Query: Yes and choose the query in Step 2
Map listing settings
Map listing settings

If all configured correctly, the map should rendering well. You can view your page in frontend and clicking the marker should show your branch details dynamically. The total markers should tally with your query builder preview result based on the settings in Step 2. Mine only shows 1 branch on the map no matter how much zoom in or out on the map.

Branch rendering on map with dynamic data in popup.
Branch rendering on map with dynamic data in popup.

Step 5: Create Search By Address Input Field

Install and activate JetSmartFilters plugin. Add a new Search type filter and follow my settings.

Search by address filter settings.

Filter Settings > Search by > By Custom Field (from Query Variable)

Query Settings > Query Variable > use a non valid meta key name. I use itchy_search_by_address and 100% sure this is not a valid custom field in entire website (current + future).

Next, add a Search Filter widget into the Map Listing page. Select the newly created search filter. Style it nicely and make sure the Query ID targeting the Map Listing widget’s CSS ID in Step 4.

Search filter widget settings.
Search filter widget settings.

As of now, your page should similar to mine. And if you do a search on a valid address, you will notice the map just rendering without any markers. Nothing wrong with your setup and great that you follow until this step!

Map with Search By Address filter.
Map with Search By Address filter.

A test search will return empty marker result.
A test search will return empty marker result.

Step 6: Use jet-smart-filters/query/final-query Filter

We can use jet-smart-filters/query/final-query filter to amend the query JetSmartFilters query. In the return $query, just need to add a geo_query array key with latitude, longitude and distance values so JetEngine can execute the geo search function. We will also use \Jet_Engine\Query_Builder\Manager::instance() to get our query builder default latitude, longitude and distance settings.

Apply these code in your child theme functions.php

<?php

add_filter( 'jet-smart-filters/query/final-query', 'itchy_amend_map_query', 20 );

function itchy_amend_map_query( $query) {

	// My filter Query ID is branch-map, only target this filter query, replace this with yours.
	if( $query['jet_smart_filters'] !== 'jet-engine-maps/branch-map' ) return $query;

	if( isset( $query['meta_query'] ) ) {

		$query_builder = \Jet_Engine\Query_Builder\Manager::instance();
		// My Geo Search query builder ID is 2, replace this with yours.
		$branch_geo_search_query = $query_builder->get_query_by_id( 2 );

		if( !$branch_geo_search_query->query ) return $query;

		// Set default query builder lat, lng and radius, make sure the geo search query has these values.
		$default = $branch_geo_search_query->query;
		$lat = ( isset( $default['geosearch_location']['lat'] ) )? $default['geosearch_location']['lat'] : 4.6170665820643;
		$lng = ( isset( $default['geosearch_location']['lng'] ) )? $default['geosearch_location']['lng'] : 101.12098574638;
		$radius = ( isset( $default['geosearch_distance'] ) )? $default['geosearch_distance'] : 10;

		$address = '';

		// Now let's get the address entered by visitor, we use the itchy_search_by_address key defined in Step 4.
		foreach( $query['meta_query'] as $meta_query ) {
			if( $meta_query['key'] === 'itchy_search_by_address' ) {
				$address = sanitize_text_field( trim( $meta_query['value'] ) );
				break;
			}
		}

		// Make sure clear the meta query because we really dont want to search branch by any fields.
		// If you don't do this, the query will return empty result because no itchy_search_by_address meta key is found.
		unset( $query['meta_query'] );

		// Only search by address if address is not empty
		if( $address != '' ) {
			$provider_id      = \Jet_Engine\Modules\Maps_Listings\Module::instance()->settings->get( 'geocode_provider' );
			$geocode_provider = \Jet_Engine\Modules\Maps_Listings\Module::instance()->providers->get_providers( 'geocode', $provider_id );
			
			// This function will help us retrieve the lat and lng of the address, suggest use Google API to get more accurate data
			$latlng = $geocode_provider->get_location_data( $address );
			
			if( $latlng ) {
				$lat = $latlng['lat'];
				$lng = $latlng['lng'];
			}
		}

		// Set the new geo search query
		$query['geo_query'] = array(
			'latitude'  => $lat,
			'longitude' => $lng,
			'distance'  => $radius,
		);

	}
	return $query;
}
?>

Read my comment, adjust to suit your project. Line 12 – 24 is my way to get some default values for lat, lng and radius from JetEngine Query Builder. You can just hardcode them directly if you want. Line 40 – 49 playing important role which instructs JetEngine to get the address’s coordinate based on your geocoding provider setting. Once again, I recommend to use Google as Geocoding provider, this is because I randomly get false result when using OpenStreetMap, maybe my country’s addresses not fully covered in their database?

Alright, now let’s do a search by using the filter. You should get branches within 10KM (my distance setting in query builder) from the address. Clicking the markers populate the branch’s details as expected!

Get 4 branches within 10 KM based on my search address.
Get 4 branches within 10 KM based on my search address.

Step 7: Allow Visitor Set Search Distance

Currently, visitor will still see empty marker result if no branch within 10KM based on the search address. I am going to add 1 dropdown field for them to choose predefined distance for each search.

Let’s create a new Select Type filter and follow my settings.

  • Data Source > Manual Input
  • Options List > Make sure value is number, label is human readable text
  • Query Settings > Query Variable: use a non valid meta key name. I use itchy_search_by_radius
  • Query Settings > Comparison operator: anything, or just leave it as Equals
Select Type filter and Manual Input as Data Source
Select Type filter and Manual Input as Data Source
Value should be number only.
Value should be number only.
Make sure itchy_search_by_radius never ever use as meta field in entire website
Make sure itchy_search_by_radius never ever use as meta field in entire website

Insert a Select Filter widget in the Map Listing page. Remember to set the Query ID to target the Map Listing correctly.

Search by radius (Select Filter) widget settings
Search by radius (Select Filter) widget settings

Okay, now let’s modify the itchy_amend_map_query function to dynamically set the distance / radius.

<?php

function itchy_amend_map_query( $query) {

	// My filter Query ID is branch-map, only target this filter query, replace this with yours.
	if( $query['jet_smart_filters'] !== 'jet-engine-maps/branch-map' ) return $query;

	if( isset( $query['meta_query'] ) ) {

		$query_builder = \Jet_Engine\Query_Builder\Manager::instance();
		// My Geo Search query builder ID is 2, replace this with yours.
		$branch_geo_search_query = $query_builder->get_query_by_id( 2 );

		if( !$branch_geo_search_query->query ) return $query;

		// Set default query builder lat, lng and radius, make sure the geo search query has these values.
		$default = $branch_geo_search_query->query;
		$lat = ( isset( $default['geosearch_location']['lat'] ) )? $default['geosearch_location']['lat'] : 4.6170665820643;
		$lng = ( isset( $default['geosearch_location']['lng'] ) )? $default['geosearch_location']['lng'] : 101.12098574638;
		$radius = ( isset( $default['geosearch_distance'] ) )? $default['geosearch_distance'] : 10;

		$address = '';

		
		foreach( $query['meta_query'] as $meta_query ) {

			// Get the address entered by visitor, we use the itchy_search_by_address key defined in Step 4.
			if( $meta_query['key'] === 'itchy_search_by_address' ) {
				$address = sanitize_text_field( trim( $meta_query['value'] ) );
			}

			// Get the radius selected by visitor, we use the itchy_search_by_radius key defined in Step 7.
			if( $meta_query['key'] === 'itchy_search_by_radius' ) {
				$radius = absint( sanitize_text_field( trim( $meta_query['value'] ) ) );
			}
		}

		// Make sure clear the meta query because we really dont want to search branch by any fields.
		// If you don't do this, the query will return empty result because no itchy_search_by_address or itchy_search_by_radius meta key is found.
		unset( $query['meta_query'] );

		// Only search by address if address is not empty
		if( $address != '' ) {
			
			$provider_id      = \Jet_Engine\Modules\Maps_Listings\Module::instance()->settings->get( 'geocode_provider' );
			$geocode_provider = \Jet_Engine\Modules\Maps_Listings\Module::instance()->providers->get_providers( 'geocode', $provider_id );
			
			// This function will help us retrieve the lat and lng of the address, suggest use Google API to get more accurate data
			$latlng = $geocode_provider->get_location_data( $address );

			if( $latlng ) {
				$lat = $latlng['lat'];
				$lng = $latlng['lng'];
			}

		}

		// Set the new geo search query
		$query['geo_query'] = array(
			'latitude'  => $lat,
			'longitude' => $lng,
			'distance'  => $radius,
		);

	}
	return $query;
}

?>

The amendment on Line 25 – 36, we retrieve the distance / radius value from the itchy_search_by_radius key defined earlier in JetSmartFilters.

Play in frontend, you will see it’s working good even without typing in address. Which means, it’s using the default location (My HQ branch in this example) as the search address, and find all nearby branches within the distance from the Search by radius dropdown field.

Who the hell wants to find 400 KM "nearby" branches? Hahahaha.., just example man~
Who the hell wants to find 400 KM “nearby” branches? Hahahaha.., just example man~

Step 8: Hide The Map If Result Is Empty

The Widget Visibility option on Map Listing only works on page load. If the new search (AJAX) has no result, the Map will be showing the default location without any branch marker and without any error message to indicate that. I will use JavaScript to fix this.

We can listen on jet-filter-custom-content-render jQuery event to check response result to decide whether show or hide certain elements. If you would like to know more JetSmartFilters events, kindly read this article.

I create a static text using Heading element, give it a CSS ID (branch-not-found) and some padding to make it looks better. Use custom CSS to set it display:none because I want this Heading widget only shows if no result from the filter.

A Heading widget place underneath Map Listing
A Heading widget place underneath Map Listing with the settings

JavaScript time! As usual, I place my JavaScript in functions.php for tutorials.

<?php
add_action( 'wp_footer', 'itchy_map_javascript' );

function itchy_map_javascript() {
	// Only generate my javascript on the map page
	if( !is_page('search-branches') ) return; ?>
		<script>
			(($)=>{
				document.addEventListener('DOMContentLoaded', ()=>{
					//We must wait for the jQuery ready event because JetSmartFilters is not initialized yet
					$(document).ready(()=>{

						// Replace with your actual element IDs
						const branchMap = $('#branch-map');
						const branchNotFound = $('#branch-not-found');

						if( branchMap.length && branchNotFound.length ) {

							// Listen on JetSemartFilters result event
							branchMap.on('jet-filter-custom-content-render', (evt, response)=>{

								if(response.markers.length < 1) 
									branchMap.hide()
									branchNotFound.show()
								 else 
									branchMap.show()
									branchNotFound.hide()
								
							}) 
						}
						
					})//end jQuery document ready
				})//DOMContentLoaded
			})(jQuery)
		</script>
	<?php
}

?>

The code very easy to understand. Try in frontend and it’s now working perfectly.

When no markers in the response result, hide the map, show the text.
When no markers in the response result, hide the map, show the text.

Conclusion

I like JetEngine Geo Search function and it truly save my time when creating such feature on client’s website. Hope they can improve the Map type field and also Geo Search Select location way in Query Builder. It’s a disaster if keep using map to pin where I want. MetaBox did this very well when handling map type field.

Hope you like this tutorial.

Useful resources used in this tutorial:

  • jet-smart-filters/query/final-query filter hook
  • \Jet_Engine\Query_Builder\Manager::instance()
  • jet-filter-custom-content-render jQuery event
  • About Author

    I like to solve problems by coding. I like websites, web applications and everything viewable from browser. Knowledge sharing can grows my confidence. A simple "thank you" will make my day :)
    More about me

Subscribe For Notification

Get notification whenever new article published.

Discussion

COMMENT

First off thank you! Is there a way to make so that the Map widget shows all of the listings (CPT's) until a user enters an address/radius to search by?

Reply
By Isaac (3 months ago)

Hi Isaac, you might try to set the default distance in Query Builder (Step 2) to a high number value. Then in Step 6, itchy_amend_map_query function replace line 22 $radius = 10; or the first value of your frontend radius option. If this doesn't works, kindly contact me via hire me or contact form. Thanks.

By [email protected] (3 months ago)

Hey, thank you so much for this tutorial. I have the same trouble like Isaac with my project. I also tried to set the default radius to a higher value, but then I have the problem, if you only enter the address / city without radius selection, the default radius (which is a high number) is obviously used, and this leads to showing all the listing elements. Would be great to somehow enter some kind of "while address field is empty, do nothing". But it shouldn't impact other filters, that I also want to use to filter my listing grid. In my case for example Checkbox, Select and Range filters. Thx in advance and best regards Thorben

Reply
By Thorben (2 months ago)

Hi Thorben, understand your problem, radius / distance is a must when using geo query, that's why I must set a value in this tutorial. However, it's possible to tweak and achieve what you need. Based on your scenario, you should remove the Geo Search setting in Step 2. Which means, by default, the query just execute without calculate or using geo search feature. In your code, set the geo query only when address field has value (Ex: In Step 6, cut line 53 - 57, place it after line 49).

Hope my clue can solve your problem. Drop me message via hire me form if need code assistance on your project.

By [email protected] (2 months ago)

Hi,
Thank you for this very nice tuto !
I have a question : did you manage to combine this custom query with other smart filters ?
For example I would like to filter the map with an other meta field using a jet smart filter. Example : find the restaurants of (meta) category "Italian" in the 5km radius of the adress. But the map listing widget must be set to query in this case, where for smart filters we set it to posts.
I tried to set the Query ID in Query Builder general settings which says "Set custom Query ID to connect this query with JetSmartFilters. To work correctly this Query ID should be the same as Query ID set in the filter settings and ID of appropriate widget where this query is used." but smart filters don't apply on the map even if set with the same query id.
If you have already experimented... Thanks !

Reply
By Fran├žois (1 month ago)

Hi Francois,
Yes I did a few customization working together with different filters at the same time.
If the other filters also a meta query search type filter, then you should tweak more in unset( $query['meta_query'] );
Only unset the meta_query for geo search, leave the correct proper meta_query in the function, so JetSmartFilters will accept that as a proper meta_query.

Good Luck.

By [email protected] (1 month ago)