How to create a WordPress location based search

January 14, 2015 | PHP, Search, Time Savers, Tutorials, Wordpress | 0 comments

I’ve done this a few times in the past but yesterday I decided to write these notes for a colleague to show how to construct a location based search for a new WordPress site he is developing. Essentially to create a location search, in this case for Property sales, we need to override the standard WordPress search with our own custom query. The following is split into sections. It’s written mostly in note form but it should be easy enough to follow. If you have any questions by all means comment and I’ll answer as best as I can.

Data

Create a new custom post type called ‘property’ which will serve as the location for the data keeping it away from the other content on the site.

Custom fields

I use Advanced Custom Fields for all of my projects these days but by all means use your own plugin or method. As long as the data is stored in wp_postmeta then this tutorial is still valid. Create something similar to the following fields in your system. These aren’t really referenced specifically later on but are a good start for search fields for this sort of site.

  • Beds (select, fixed number of options)
  • Baths (select, fixed number of options)
  • Price (number field)
  • Address (separate fields unless you want to split them manually)
  • Type (select, predefined, multiselect?)
  • Status (select, predefined… under offer, sold, available?)

Custom data

On saving a new/exsiting property post type item you need to geocode the address and save the resulting latitude and longitude into the wp_postmeta table. Use the following page to help you with this:

http://codex.wordpress.org/Plugin_API/Action_Reference/save_post

Use the ‘book’ example looking for the ‘property’ post type almost verbatim. It will show you how to trigger a PHP function on the saving of a new or existing item of content using the correct post type. You then need to Geocode (convert to latitude and longitude) the address string from the POST array and save in postmeta per above. The following page will help you with that.

http://www.andrew-kirkpatrick.com/2011/10/google-geocoding-api-with-php/

If you aren’t familiar with how to save to postmeta then the following will help:

update_post_meta($post_id, 'latitude', $latitude);

Build the search form sidebar

This can be done either in raw PHP in sidebar.php (or somewhere in the theme) or as a widget (latter method preferred for sidebars as makes it portable). If widgets then the following will help significantly as the code sample is more or less exactly what you need:

http://codex.wordpress.org/Widgets_API

Copy the code from the section titled ‘Example’. This will register a widget on the system which you can drag into one or more sidebars in Appearance > Widgets

Setting up a sidebar is here:

http://codex.wordpress.org/Function_Reference/register_sidebar

If you are using the Bones theme (my preference) then it has several set up out of the box. A sidebar has a name which you use to put the contents onto the site. The code is simply:

dynamic_sidebar($sidebar_name);

It will echo by default and outputs nothing if no widgets so nice and easy really

Build the search form

You can do in HTML/CSS as normal hard coding the values of the search or pulling from $_GET which could be where you are advised to send the data through (means you can directly link via URL to a search result rather than POST which is less portable). The form action to use for a search in WP is /?s= which will activate the search template. ‘s’ is the querystring parameter for a search term. You can perform a search without it.. A handsome chappie wrote a code sample you can use:

/2012/05/wordpress-search-keyword

Perform the search

search.php in your theme powers the search results as you might know. The query itself can be done anywhere so use this for layout purposes. There should be ‘the loop’ in this file as normal which is designed for search results. Before ‘if (have_posts()): the_post();’ add a PHP function call or your own to something you have defined in functions.php or simply dump your search code into search.php.. there is no ‘wrong’ way to do it.

The trigger for the search is a WP function ‘query_posts()’. It will override the array of data that ‘has_posts’ has to work with and you can use the native output loop/functions to display the results.

Constructing a custom search would be a case of building an array and passing to query_posts. You can get most of the search done this way and then split into a bit of SQL perhaps for the location based side of things. I shall go into that later.

The resource you need to map the majority of the fields would be:

http://codex.wordpress.org/Class_Reference/WP_Query

WP Query is the class which WP uses at the back of query_posts but using the latter function sets up globals which make WordPress behave differently in ‘the loop’. For searching on data stored in postmeta you would need to use ‘meta_query’, taxonomies use ‘tax_query’ and general stuff like search you just pass as attributes to the array. Examples as follows which you can break down and use if you like:

function cf_custom_search_query() {
	global $wp_query; //not sure we need this
	
	$arr = array();
	
	$author = cf_get('author'); //cf_get is a wrapper on if (isset(something)) return something; else return false;
	$genre = cf_get('genre');
	$price_from = cf_get('price-from');
	$price_to = cf_get('price-to');
	
	$paged = get_query_var('paged'); //wp function. copy verbatim
	
	$arr['post_type'] = 'property'; //post type slub
	$arr['post_status'] = 'publish'; //published only as opposed to draft or private
	$arr['paged'] = $paged; //sets the page number to show.. just leave this as is
	
	$condition = array();
	
	foreach ($_REQUEST as $key=>$value) {
		if (substr($key, 0, 5) == 'cond-') {
			$condition[] = substr($key, 5); //for multi select checkboxes
		}
	}
	
	if ($val = cf_get('s')) {
		$arr['s'] = $val; //s is the actual search term
	}
	
	$meta_query = array();
	
	if ($price_from || $price_to || $author) { //meta query is for items in post_meta. we can only pass a single array although the array can have multiple conditions
		if ($price_from && $price_to) {
			$meta_query[] = array(
						'key'     => '_price',
						'value'   => array($price_from, $price_to),
						'type' => 'DECIMAL',
						'compare' => 'BETWEEN'
					);
		} else if ($price_from) {
			$meta_query[] = array(
						'key'     => '_price',
						'value'   => $price_from,
						'type' => 'DECIMAL',
						'compare' => '>='
					);
		} else if ($price_to) {
			$meta_query[] = array(
						'key'     => '_price',
						'value'   => $price_to,
						'type' => 'DECIMAL',
						'compare' => '<='
					);
		}

		if ($author) {
			$meta_query[] = array(
						'key'     => 'author',
						'value'   => $author,
						'type' => 'CHAR',
						'compare' => 'LIKE'
					);
		}
		
		if ($meta_query) {
			$arr['meta_query'] = $meta_query;
		}
	}
	
	if ($genre && $genre != '-') { //this is a custom taxonomy. so we pass the category slug (or an array of slugs) from a category.
		$arr['tax_query'][] = array(
			'taxonomy' => 'product_cat',
			'field'    => 'slug',
			'terms'    => $genre,
		);
	}
	
	if ($condition) { //as above. another custom taxonomy
		$arr['tax_query'][] = array(
			'taxonomy' => 'condition',
			'field'    => 'slug',
			'terms'    => $condition,
		);
	}
	
	if (!$sort = cf_get('sort_order')) {
		$sort = cf_session('sort_order', 'price_low');
	}
	
	if ($sort) {
		
		if ($sort == 'price_low' || $sort == 'price_high') {
			$arr['orderby'] = 'meta_value_num';
			$arr['meta_key'] = '_price';
			$arr['order'] = 'ASC';
			
			if ($sort == 'price_high') {
				$arr['order'] = 'DESC';
			}
		} else {
			$arr['orderby'] = 'title';
			$arr['order'] = 'DESC';
			
			if ($sort == 'abc') {
				$arr['order'] = 'ASC';
			}
		}
		
		$_SESSION['sort_order'] = $sort; //so it's saved for next time if they go elsewhere and come back
	}
	
	query_posts($arr); //performs the query and sets up the globals
}

Performing a location based search

On search we have ‘s’ or ‘location’ depending on how you built your form. Up to you which field you use but perform the same Geocode on the fly as you did for the save_post step above. This will give you a latitude and longitude to search on. To get into the actual SQL of a search you would want to implement something like the following:

http://codex.wordpress.org/Plugin_API/Filter_Reference/posts_where //to add to WHERE
http://codex.wordpress.org/Plugin_API/Filter_Reference/posts_join //to add to FROM

A handy usage conversation to help with the above:

http://wordpress.stackexchange.com/questions/75381/using-dynamic-conditions-in-posts-where-filter

This is a good resource giving you all of the filters you can use to get access to the SQL of the main query:

http://codex.wordpress.org/Plugin_API/Filter_Reference/posts_clauses

Make sure to wrap your add_filter and remove_filter calls in is_search() which means you only require the LAT/LONG calculation to be done in search based queries.. or just only add/remove the filter in your search function defined in functions.php or search.php and only called on a search.. making sure to remove once used.

if (is_search()) { 
	add_filter('posts_where', 'my_posts_where'); 
	//.. and so on
	//perform search function
	remove_filter('posts_where', 'my_posts_where'); 
	//.. and so on 
}

The remove is just housekeeping, however, if you don’t remove the filter(s) then the next time you run query_posts in that same page load the same filters will be added which might mess with the result unexpectedly. To put this together you need to join in two instances of wp_postmeta or $wpdb->postmeta (in case wp_ is not the table prefix) making sure to alias uniquely:

add_filter('posts_join', 'my_posts_join');

function my_posts_join($sql) {
	$sql .= ' JOIN ' . $wpdb->postmeta . ' my_join1 ON (' . $wpdb->posts . '.ID = my_join1.post_id AND my_join1.meta_key="latitude");
	return $sql; 
}

And in posts_where:

add_filter('posts_where', 'my_posts_where');

function my_posts_where($sql) {
	$sql .= ' AND my_join1.value = "blah" '; //always start AND because it will be appended onto the main query and you can use the aliases defined in the post_join above
	return $sql; 
}

You can use the geocode values for the location entered via $_GET and then write the SQL to return the correct result based of something like this example:

https://developers.google.com/maps/articles/phpsqlsearch_v3

The section titled ‘Finding Locations with MySQL’ gives the query and some useful advice.

Get a brew and put your feet up!

A Donate Button!

0 Comments

Leave a Reply

Divi United

About this site and Sean Barton

Picture of Sean
Sean Barton is a Freelance PHP Website Developer in Crewe, Cheshire. He is a Wordpress and CMS/Framework specialist.
This site was set up in 2008 as a tutorial and scripting resource for the PHP language and Wordpress.
Find out more about Sean on the About Me page or use the Hire Me page to get in touch. For more information about Sean's work take a look at the Portfolio

Our Services

  • Wordpress plugin/theme development
  • Divi specialist
  • Ecommerce (Woocommerce, WPSC, Shopify, Magento)
  • PSD to Wordpress theme conversion (Responsive)
  • Website design work (Banners, Logos, Full Site, etc)
  • Website analysis (security, usability, SEO)
  • API Integrations (InfusionSoft, SalesForce, Ontraport, Customer Thermometer, etc..)
  • Wordpress consultancy & expert advice
  • Crisis support
  • Website hosting

The main services offered are Wordpress based although we do a great deal of technical programming for bespoke systems. From troubleshooting, extending frameworks, finding bugs to writing them from scratch.

Find out more by looking through our past projects or get a quote.

Be the first to hear about new products/updates!

This is a mailing list for those people interested in being told when we release a new product (Divi plugin or Theme).

We shall also use this list to let you know about product updates and releases.

You have Successfully Subscribed!