Performing a radial search (showing results based on location, zip code, etc) in WordPress is one of the hardest things I’ve had to figure out how to do. It held me back in many projects and was always a source of frustration.
After trying many different plugins and tutorials, I have finally come up with a working solution. Today I’m going to share that with you.
This solution includes many different components. They are as follows:
1. A PHP function to calculate the distance between two zip codes.
2. A javascript function to generate the link to our results page with given parameters.
3. The HTML form to search by our parameters.
4. Getting parameters from the URL
5. A WordPress loop to run the distance calculation on our posts.
6. Another WordPress loop to actually display our results.
As you can see it’s pretty complicated, but I’m going to break it down for you. You may notice I’m using functions like get_field or the_field in some of the PHP. Those are shortcut functions to get custom meta data from our posts and are generated from the brilliant Advanced Custom Fields plugin. You do not need to use this plugin, just be aware that you will need to alter your code to get the meta values in your situation. So let’s get to the code.
Calculating the distance between two zip codes.
Fortunately for us, Google provides a free API for looking up a ZIP code and providing the GPS coordinates. Originally I had believed that you could simply calculate the location of a ZIP code based on its value but that, as it turns out, is not how it works.
Fortunately there is a premade chunk of code that ties into Google’s API and returns those values for us. I was unable to find the original source, but there are a bunch of random sites sharing the same code. Here it is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
<?php // This function returns Longitude & Latitude from zip code. function getLnt($zip){ $url = "http://maps.googleapis.com/maps/api/geocode/json?address= ".urlencode($zip)."&sensor=false"; $result_string = file_get_contents($url); $result = json_decode($result_string, true); $result1[]=$result['results'][0]; $result2[]=$result1[0]['geometry']; $result3[]=$result2[0]['location']; return $result3[0]; } //Gets the distance between two zip codes function getDistance($zip1, $zip2, $unit){ $first_lat = getLnt($zip1); $next_lat = getLnt($zip2); $lat1 = $first_lat['lat']; $lon1 = $first_lat['lng']; $lat2 = $next_lat['lat']; $lon2 = $next_lat['lng']; $theta=$lon1-$lon2; $dist = sin(deg2rad($lat1)) * sin(deg2rad($lat2)) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * cos(deg2rad($theta)); $dist = acos($dist); $dist = rad2deg($dist); $miles = $dist * 60 * 1.1515; $unit = strtoupper($unit); //Personally I disable this line. I'm not sure why you would want to display "MILES" in all caps. if ($unit == "K"){ return ($miles * 1.609344)." ".$unit; } else if ($unit =="N"){ return ($miles * 0.8684)." ".$unit; } else{ return $miles." ".$unit; } } ?> |
As you can see, the first function, getLnt, finds the latitude and longitude of the zip code. The second function, getDistance, then calculates the distance between that and a second zip code. We will use this later to calculate the distance between two dynamic points.
Generating the parameter-filled link to our results page
Let’s set up the javascript function to generate a link to our results page. This function does a couple things. First, it gets the values from our form that we are setting up later and adds them to an array. This array is then appended to the target url and will later tell our function what values to use.
To customize this for your own use, replace the IDs with your own element’s HTML IDs. For example, your input ID for the postal code input may not be “postal_code”, so you would need to change that.
The URL at the end will need to be your results page. For me, I actually want the search page and the results page to be one and the same. All of the code I am sharing here resides on a page template, which I have selected for use on this page. It might not be the best practice, but for illustration purposes it’s the easiest to work with that way.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<script type="text/javascript"> function SubmitFrm(){ var Searchtxt = document.getElementById("country").value; window.location = "http://www.div12.org/therapist-search/?country=" + Searchtxt; } function ZipDistance(){ var URLstring = "" if (document.getElementById("postal_code").value != '') {URLstring += "&zipcode=" + document.getElementById("postal_code").value; } if (document.getElementById("SearchDistance").value != '') {URLstring +="&distance=" + document.getElementById("SearchDistance").value; } window.location = "http://www.example.com/results-page/?" + URLstring; } </script> |
The “SearchDistance” variable is one that I have set up to change the distance the user wishes to search. For example, if you only want to display results from within 50 miles, you enter it in SearchDistance.
Displaying the HTML search form
This part is pretty straight-forward. You just want to make sure that your input IDs are correct and matched up to the javascript function from earlier. I put everything in a table to keep it neat. I also added the values to these, which will get the value from the URL and repopulate the inputs with the proper value, if you have already performed a search. This is great for usability so that users don’t have to re-enter information if they wish to alter their search.
1 2 3 4 5 6 7 8 9 10 11 |
<h3>Results Near Me</h3> <table> <tr><td><p>Zip Code</p></td><td><p>Maximum Distance from Me (miles)</p></td></tr> <tr><td> <input name="postal_code" type="number" id="postal_code" class="field" value="<?php echo $_GET['zipcode'] ?>" /> </td><td> <input name="SearchDistance" type="number" id="SearchDistance" class="field" value="<?php echo $_GET['distance'] ?>" /> </td><td> <input type="submit" name="btnSearch" value="Go!" id="ZipDistance" class="btn" onclick="javascript:ZipDistance()" /> </td></tr> </table> |
Getting parameters from the URL
Now we need to pass the parameters from the URL into our query so that we can find the correct results. Here’s what I have for the two parameters I am using:
1 2 3 4 5 6 7 8 9 |
<?php //Get the parameters from the URL if(isset($_GET['distance'])) { $proximity = $_GET['distance']; } if(isset($_GET['zipcode'])) { $zipcode = $_GET['zipcode']; } ?> |
This section will get the parameters we passed into the URL and turn them into PHP variable for later use. It first checks to make sure they are set in the URL, and THEN sets them to a parameter.
Calculating the distance from each of our posts
Now comes the fun part, and the part it took me ages to figure out. I even sought help at Stack Overflow, but nobody seemed to know how to begin the process of running a calculation on every post before displaying them.
The simple solution, I realized, was to run two loops. The first would go through each potential result, run the calculation on it, and then add it to an array of posts to be queried again if it passed the calculation. So for this first loop, we can add whatever we want to a standard $args array to do a preliminary filter on our posts. In my case, I’m just going to filter by page template and another meta key value unrelated to this tutorial. You can modify yours to filter for anything you like.
Then, in the loop itself, we just run our distance calculation and add passing post IDs to our $results string, which we will use later in our second loop.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
$args = array ( 'post_type' => 'page', //My results happen to be a page, but you may be searching posts 'posts_per_page' => -1, //Important - we need ALL results to display without pagination 'meta_query' => array( 'relation' => 'AND', array( 'key' => '_wp_page_template', 'value' => 'therapist.php', 'compare' => '=' ), array( 'key' => 'other_meta_key', 'value' => $value, 'compare' => 'LIKE' ), ), ); $results = array(); if (isset($zipcode)) { //Make sure a zipcode is set. No need to run the search if the user hasn't entered anything yet $the_query = new WP_Query( $args ); // The Loop $posts = $the_query->get_posts(); foreach($posts as $post) { $zip1 = get_field('postal_code'); //Remember, I'm using Advanced Custom Fields which allows me to use get_field $zip2 = $zipcode; $unit = 'Miles'; //Specify Units if (isset($zip1)) {$distance = getDistance($zip1, $zip2, $unit); } //Calculate distance using our function if ( $distance < $proximity && isset($zip1)) { //If the distance is less than our threshold, then we are going to add it to our array array_push($results, get_the_ID()); }} wp_reset_postdata(); //Reset the query, so we can run our second loop later } |
Now we have an array of post IDs that we can use to query in our second loop, which will actually display our results. This should be friendly with pagination as well, and since it won’t be querying any superfluous posts, won’t add too much load time either.
Displaying the final results on the page
Now we just run our final loop to display the results. The $args are going to be pretty straightforward. You can use the parameter post__in to search for posts with the IDs that we have stored up in our $results array. Here is the code for the whole thing. Keep in mind, you can adjust the section that actually displays the results however you want. This is just how I did it!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
<?php $args2 = array( 'post_type' => 'page', 'post__in' => $results //This is where the magic happens. Get all the posts we found from the last loop ); $the_query2 = new WP_Query( $args2 ); //You are going to need to have the 2 on these to make sure they don't conflict with the first loop $zip2 = $zipcode; $unit = 'Miles'; // The Loop if ( $the_query2->have_posts() ) { while ( $the_query2->have_posts() ) { $the_query2->the_post(); ?> <article id="post-<?php the_ID(); ?>" <?php post_class('clearfix'); ?> role="article"> <header> <h3 class="h2"><a href="<?php the_permalink() ?>" rel="bookmark" title="<?php the_title_attribute(); ?>"><?php the_title(); ?></a></h3> </header> <!-- end article header --> <section class="post_content"> <?php $image = get_field('custom_image_meta'); //I have a custom meta field that is an image. The following code displays it properly with a link to the post if( !empty($image) ): // thumbnail $size = 'thumbnail'; $thumb = $image['sizes'][ $size ]; $width = $image['sizes'][ $size . '-width' ]; $height = $image['sizes'][ $size . '-height' ]; ?> <a href="<?php the_permalink() ?>" rel="bookmark" title="<?php the_title_attribute(); ?>"><img src="<?php echo $thumb; ?>" alt="<?php echo $alt; ?>" width="<?php echo $width; ?>" height="<?php echo $height; ?>" style="float:left; padding: 0 10px 10px 0;" /></a> <?php $zip1 = get_field('postal_code'); $distance = getDistance($zip1, $zip2, $unit); if (isset($distance)) { $distance = number_format( $distance, 0 ); ?> <p style="font-weight:bold; margin-top:0; margin-bottom:0;"><?php echo $distance; ?> Miles</p><?php } ?>/* Might be nice to display the calculated distance for each result */ <?php endif; ?> <?php the_excerpt(); ?> </section> <!-- end article section --> <footer> </footer> <!-- end article footer --> </article> <!-- end article --> <?php } } /* Restore original Post Data */ wp_reset_postdata(); } else { ?><h1 style="color:#555;" class="no-results">No Results Found</h1><?php //We should probably say if there are no results } ?> |
And that should be it! There’s a lot to it but it’s not so hard if you break it down. The nice thing about this method is that you can fairly adapt it to lots of other uses and custom queries. In a site I just built, I also have more complicated things like select boxes where the options auto-populate based on existing, in-use values. Let me know if this works for you, or if you have any questions!
11 Comments on “Radial Search & Results Page – WordPress Tutorial”
This is exactly what I am looking to do. Do you have a link to see a demo/working?
At the moment, no. I’ve used it on a number of client sites successfully, but I don’t have permission to share them here. That could also be a security issue if someone were to find a vulnerability in this code.
Could you explain how I might show results that fall between a certain distance range? See between 25 and 50 miles away…
Hi, Brian. Thanks for sharing this, it has been very helpful. Just wondering how to go about sorting the results from nearest first? Would love some tips. Thanks.
woogeolocation.com is a scam website
Excellent post. Thank you for this, I was racking my brain to try and work out the logic/components I needed to go through to get something similar working for a real estate site – also a fan of ACF
Thanks, Oliver. Glad I could help!
Very interesting solution. I have something in common. Don’t know yet how to solve it. I am using Beaver Builder pagebuilder and use Beaver Themer to build custom archive templates in combinations with the use of a CPT and CF’s (made in ACF). I do use Search&Filter Pro to filter on two fields I created: height and width (the site is about calculating glass windows). The filter works fine on the existing fields height and width to determine which window types fall in that range. However their is also the surface of the window. Within ACF in the backend I made a field and in that field I made the little PHP calculation and store the value in that field.
Howver I don’t wnat to have the people searching in the front-end to fille in the surface theirself based on the given height and width. So in the search a kind of a hidden value (or field) should do the clacultion for the surface and then filter the results. Any idea how I can do that?
Hi Brian,
I’m building a new website for multiple vendors to sell their goods locally. Should this become bigger than the twin cities, I would like customers to be able to search for products within a certain proximity/radius to their home since they’ll be picking up the items. Similar to groupon.com
I see you have thoughts on a radial search on WordPress…. I’m wondering if this would work with the Flatsome theme http://preview.themeforest.net/item/flatsome-multipurpose-responsive-woocommerce-theme/full_screen_preview/5484319 (I’m using Vendor Shop)
Your help is appreciated!!
Anna
It’s good to see other people from the Twin Cities!
For this type of integration, I would recommend going with something already-built for WooCommerce such as the WooCommerce Geolocation plugin here: http://woogeolocation.com/woocommerce-geolocation/
It should have the functionality you need, and you’ll just need to tweak it. I assume you’re using WooCommerce since you’re using Flatsome which is a theme I’ve actually worked with before. The theme shouldn’t really matter, but you might as well use something that integrates fully and easily with WooCommerce.! Good luck!
Hi brian, I purchased and set up the woocommerce geolocation plugin but it did not work for me at all. What’s worse, the seller has the worst customer service ever. I have contacted them about the problem I am having with their plugin but they have yet to respond. it’s so irritating.