Skip to Main Content

Search by Location with WP Query

I get a lot of client requests for “Store Locators” or “Location Finders”. There are some plugins like MapPressย and WP Store Locator that work well for simple Location Searches, but as soon as the client has requests for specific functionality or designs, these pre-made options start to fall short, and you end up spending more time fighting their code than writing your own.

What makes it especially frustrating is that, using the Google Maps API most of the features of a store locator or location finder are fairly easy to write. The only hard part is getting posts that are within a certain distance from a specific latitude and longitude. This requires a somewhat complicated trigonometric formula called The Haversine Formula, to calculate distance between two points on a sphere.ย  All the other filters and selections can be handled with a simple WP Query, but distance is a trickier beast.

ADVERTISEMENT:

Most of the existing plugins that I’ve seen do a great job at the distance calculation, but don’t include much of a framework for building more complicated queries that match post meta and taxonomies. So I wrote one that comes at things from the other direction. It takes the standard WP Query that offers so much flexibility, and adds a geo_query option that will calculate distance from a point.

You use it like this:

<?php
$query = new WP_Query(array(
	// ... include other query arguments as usual
	'geo_query' => array(
		'lat_field' => '_latitude',  // this is the name of the meta field storing latitude
		'lng_field' => '_longitude', // this is the name of the meta field storing longitude 
		'latitude'  => 44.485261,    // this is the latitude of the point we are getting distance from
		'longitude' => -73.218952,   // this is the longitude of the point we are getting distance from
		'distance'  => 20,           // this is the maximum distance to search
		'units'     => 'miles'       // this supports options: miles, mi, kilometers, km
	),
	'orderby' => 'distance', // this tells WP Query to sort by distance
	'order'   => 'ASC'
));

In addition, there are two included functions that can be used to retrieve distance in the resulting loop: the_distance() and get_the_distance(). Both support two optional variables: a post object, and the number of integers to round the distance to.

The full code for the plugin is included below.

42 Comments

    • gschoppe's profile image.

      I’m working on a plugin to simplify the process, but in general, you would use the google maps api (i like using gmaps.js to simplify the API calls) to geocode a search field, into latitude and longitude, then, once you have lat and lng, you send them to an ajax function that runs the query from this post.

      • DaniB's profile image.

        Hello again! I’m revisiting this code because I need to hook my posts up to a google map search field. I have set it up per your suggestion on an earlier comment (send the lat/long to an ajax function that runs the query) however when I get the results back the posts are not sorted, and the output from the_distance(); is blank, even if I’m just running the same query with hardcoded lat/long on submission of the ajax form. Is there anything you can recommend I try to fix this, or were you ever able to complete the plugin mentioned in your previous comment to simplify it?
        Please let me know! This is an amazing plugin, thank you so much for sharing it. I almost have everything working how I need it to ๐Ÿ™‚

        • gschoppe's profile image.

          Sorry to be replying to this so late, but my guess is that you passed the post ID to get_the_distance(), rather than the post object itself. I can’t really be sure without seeing your implementation of the loop.

  1. chris's profile image.

    Can this be used in combination with Toolset maps? I’m using Toolset to create custom posts, but want to sort them by distance.
    So when a user searches a city it takes the center of the city and sort by distance from that point..

    link to toolset

  2. Chris's profile image.

    This is awesome and exactly what I was looking for. I got it working but am trying to figure out how to get the get_distance() to round off? I can return the value but it has like 15 digits after the decimal point. How do I round it?

    • gschoppe's profile image.

      get_the_distance() and the_distance() each have a second argument for rounding. So, if I want to use the current global post object, and echo the distance to two decimal places, I would use the_distance(null, 2); If I wanted to make sure it was always exactly two decimal places, i would use echo number_format(get_the_distance(null, 2), 2, '.', '');.

    • gilli's profile image.

      Do you use it with toolset? Is it possible to get the modifications you made because i have a problem with toolset, it tooks to long with 20000 entries to get results on the map.

      kind regards
      gilli

      • gschoppe's profile image.

        It sounds like you are running into the limitations of doing a geo_query that relies on WordPress’s post_meta table, to perform faster for large data sets, you would want to do a few things:

        1. store latitude, longitude, and associated postID into a dedicated table with indexes on lat and lng
        2. modify the query to join this external table on postID, rather than doing two joins on the meta table, and refactor the rest of the query to reference the latitude and longitude fields in that table
        3. add a basic “bounding box” term to the search query, to allow SQL to quickly exclude all posts not within a bounding box before doing the more complex distance calculation

        I have been toying with the idea of extending this code to optionally support this sort of implementation. it’s no where near as easy to implement as a user, so I have put it off for a long time, but it is something I may come back to.

  3. Rob's profile image.

    Hi, when I install the plugin it works fine, but wp asks me to update it, and it takes me to https://wordpress.org/plugins/geo/ which is well outdated, matter of fact, when I update it, it doesn’t work anymore. Can we avoid to assk for updates or can you create your own updatable plugin? Thanks a lot

    • gschoppe's profile image.

      Hi Rob,

      Plugin updates are identified by filename, so it sounds like you saved the plugin file as `geo.php`, which matched the filename of that outdated plugin. If you change the filename to `wp-geo-query.php` the notice about an available update should go away.

  4. Berend's profile image.

    Is it possible that this class causes an inaccurate $wp_query->found_posts number? I’m getting non-zero numbers in there when the actual results are zero based on the geo_query. The $wp_query->posts array is empty.

    • gschoppe's profile image.

      it seems unlikely that the found posts count would be inaccurate as all the filtering and trimming is wholly contained in the SQL itself. Would you be able to share an example of code that is showing you an incorrect result?

      • Lane's profile image.

        I actually see the same thing – found_posts comes out as null where it didn’t before using this plugin. I still get the results I’m expecting but the value for found_posts is oddly gone.


        // query the posts
        $args = array(
        'posts_per_page' => $limit,
        'orderby' => 'post_title',
        'order' => 'DESC',
        'post_type' => 'posts',
        'post_status' => 'publish',
        'geo_query' => array(
        'lat_field' => 'wpcf-latitude', // this is the name of the meta field storing latitude
        'lng_field' => 'wpcf-longitude', // this is the name of the meta field storing longitude
        'latitude' => $lat, // this is the latitude of the point we are getting distance from
        'longitude' => $lng, // this is the longitude of the point we are getting distance from
        'distance' => $dist, // this is the maximum distance to search
        'units' => 'miles' // this supports options: miles, mi, kilometers, km
        ),
        );

        // Execute our query
        $the_query = new WP_Query( $args );

        $loop = array();
        $count = 0;
        // Loop through the results
        if ( $the_query->have_posts() ) {
        $count = $this_query->found_posts;

        while ( $the_query->have_posts() ) {
        $the_query->the_post();
        $post_id = get_the_ID();

        $loop[] = array(
        'title' => get_the_title(),
        'link' => get_the_permalink(),
        'lat' => get_post_meta( $post_id, 'wpcf-latitude', true ),
        'lng' => get_post_meta( $post_id, 'wpcf-longitude', true )
        );
        }
        $response = json_encode( array( 'status' => 'success', 'count' => $count, 'user_lat' => $lat, 'user_lng' => $lng, 'results' => $loop ) );
        }

        And my json comes out:


        {
        "status": "success",
        "count": null,
        "user_lat": "22.0000",
        "user_lng": "-85.0000",
        "results": [
        ....
        ]
        }

        • gschoppe's profile image.

          Hi Lane,

          This response might be a bit late, but it appears that you have an error in your code, where you run $count = $this_query->found_posts;. Everywhere else in your code, the query is referred to as $the_query, so $this_query isn’t initialized.

  5. Roberto's profile image.

    Hi, using this plugin, and I absolutely thank you once again for this. I have one question in regards of REST, given 2 web sites, SiteA and SiteB, let’s say I want to get SiteB content within SiteA using REST, what I receive is a big json with all SiteB posts proprieties, I have even added custom fields in the json in order to read them by parsing the json in SiteA. Now the big question: how can I use the geoQuery to query the received URL json from SiteB in order to display its content on SiteA but queried? Thanks a lot

    • gschoppe's profile image.

      Sorry for the late response. I only just now saw this comment trapped in my spam filter.

      There is no built-in REST endpoint for geo_queries, so you would need to register a custom REST endpoint on SiteA, using register_rest_route()

      • Rob's profile image.

        Thanks for the reply. Could you provide an example? Because the way I have ended up is by using the formula with JS and do the geo query using that. I’m not sure how would you register any end point to geo query. Would you be able to post an example in regards? Thank you.

        • gschoppe's profile image.

          The top comment on the documentation page for register_rest_route() shows an implementation of a custom route that includes latitude and longitude. The callback shown there could be fairly easily modified to make a query using this plugin, then convert the results to json, and echo them to the browser.

  6. Koji's profile image.

    I hit a bit of a weird issue while using this plugin while injecting “geo_query” data into a query via the action ‘pre_get_posts’.


    function insert_geodata( $query ) {
    if ( is_admin() || !$query->is_post_type_archive( 'location' ) )
    { return; }

    $geoQuery = array(
    array(
    'lat_field' => 'location_latitude',
    'lng_field' => 'location_longitude',
    'latitude' => '46.316',
    'longitude' => '-72.6833',
    'distance' => '25',
    'units' => 'km',
    )
    );

    $query->set( 'geo_query', $geoQuery );
    $query->set( 'orderby', 'distance' );
    }
    add_action( 'pre_get_posts', 'insert_geodata' );

    So… Yeah! Just a heads up; the WP Geo Query plugin doesn’t seem to catch this.

    • gschoppe's profile image.

      I’m sorry to be replying so late, but I only just saw this comment caught in my spam filter. The issue you are having is that unlike a meta_query or tax_query, a geo_query is an array of values, not an array of arrays. The double array syntax that you’re using is causing it to fail. This does raise some interesting possibilities for more complex searches, however, so I may look into modifying this code in the future to work with multiple concurrent geo_queries.

    • RTTMAX's profile image.

      I got it working.


      add_action('pre_get_posts','insert_geodata');

      function insert_geodata($query) {

      if ( is_admin() || !$query->is_main_query() || $query->query['post_type'] !== 'partner'){
      return;
      }

      $query->set('geo_query', array(
      'lat_field' => '_latitude', // this is the name of the meta field storing latitude
      'lng_field' => '_longitude', // this is the name of the meta field storing longitude
      'latitude' => 45.473450, // this is the latitude of the point we are getting distance from
      'longitude' => 7.949802, // this is the longitude of the point we are getting distance from
      'distance' => 8, // this is the maximum distance to search
      'units' => 'km' // this supports options: miles, mi, kilometers, km
      ));

      $query->set( 'orderby', 'distance' );

      }

      The key is to to it only in the main query ($query->is_main_query()).

  7. R Rae's profile image.

    I have a custom table where i keep latitude, longitude along with post ID, unique row ID and other info in their own columns. Getting one row will return all of the location data as well as other info at once. I want to work with this code to adapt it to our custom table. Where do you think would be the best place to start?

    • gschoppe's profile image.

      You’d need to modify the queries pretty heavily to work with a dedicated table. The general concept would be to do an inner join between your custom table and the post table (replacing the two inner joins on line 50 and 51), strip out the part of line 77 that filters by meta_key, and modifying the haversine formula to use the correct fields from your dedicated table.

  8. Oscar's profile image.

    Hello Greg, thanks for your superb job! I’m trying to adapt your solution for using it with Custom Post Types, but I can’t get it working properly. Do I need to modify any code for using CPT?

    • gschoppe's profile image.

      You should be able to apply this plugin to a CPT by simply adding the ‘post_type’ argument to the wp_query, as usual. If you are still having issues, just shoot me an email with your custom query code in it to [email protected], and I’ll see what I can figure out.

  9. DaniB's profile image.

    Hello! Quick question regarding this. In my theme I have a custom post type and the latitude and longitude fields are set up with Advanced Custom Field’s Google map field. With ACF the lat and long values are stored together in one meta field, and I would get them like this normally:

    $gmap = get_field(‘google_map’);
    $lat = $gmap[‘lat’];
    $lng = $gmap[‘lng’];

    Would you have an idea on how I would modify your code to use the values from those fields? Everything I’ve tried so far has sadly turned up zero results. Any help is appreciated!

    • gschoppe's profile image.

      As I understand it, ACF normally stores Lat and Lng as part of a single serialized postmeta field, which isn’t really usable for geo queries. To fix this you would need to add a snippet to your functions.php that updates the value of a hidden latitude field and longitude field, whenever ACF saves a map value. You can see code for this in the first response to the linked forum post.

      You would also need to run through the site making sure this hidden field is set for any map items that have already been saved.

      Once this is complete, you could use my geo_query plugin to do the actual search, by just setting the fields to your hidden lat field and lng field.

      • DaniB's profile image.

        Hi Greg, thank you so so much for your help! That did the trick. In case anyone else has the same issue and to avoid my frustration, there’s one small correction to the answer in the link.

        add_action('acf/update_value', 'wpq_update_lng_and_lat', 99, 3 );

        function wpq_update_lng_and_lat( $value, $post_id, $field ) {
        if( 'google_map' === $field['type'] && 'google_map' === $field['name'] ) {
        update_post_meta( $post_id, 'my_lat', $value['lat'] );
        update_post_meta( $post_id, 'my_lng', $value['lng'] );
        }
        return $value;
        }

        You need to return $value otherwise when you save your ACF fields the data will disappear.

        Thank you so much for this plugin :)

  10. Mark van der Zande's profile image.

    Hai,

    We got a problem with the ordering, we want a second ordering in the wpquery.

    ‘orderby’ => array( ‘menu_order’ => ‘ASC’, ‘distance’ => ‘ASC’ ),

    But, it dont order on distance anymore after this.

    Can you help us?

    • gschoppe's profile image.

      Hi Mark,

      Unfortunately, the plugin as it currently stands only supports a single order parameter. WordPress actually makes combining custom orderings fairly difficult, since their native ordering functions use a whitelist to strip any custom orderings out of the stack. To get combinations of orders working properly, the posts_orderby() function in the plugin would need to be modified to completely reimplement the default ordering behavior of WordPress’s class-wp-query.php (lines 2279 – 2365), along with the protected functions that are referenced in that code. Even then, it could potentially cause conflicts with other plugins that modify the behavior of the orderby attribute. This solution is also fragile because it involves duplicating several hundred lines of WordPress’s core logic. If this code was updated in the future, this could lead to unforeseen instabilities or security issues.

      If you need to implement complex ordering like this, it is probably easiest to do it by creating your own custom value for $query->orderby and then parsing it into raw SQL, by attaching your own filter to posts_orderby. Here is an example of what that would look like if included in your functions.php file:

      // implements a custom query orderby value of 'menu_order_then_distance'
      add_filter( 'posts_orderby', function( $sql, $query ) {
      global $wpdb;

      if( $query->get('orderby') == "menu_order_then_distance" ) {
      $sql = "{$wpdb->posts}.menu_order ASC, geo_query_distance ASC";
      }
      return $sql;
      }, 10, 2 );

      Please note, this snippet has not been tested, so it might include silly syntax errors.

  11. Florian's profile image.

    Hi,
    First, Thanks for this beautiful plugin.

    I need to know if there is a way yo user oerderby with a query of two post_type ?
    The orderby work if there is only one post_type but if i add one using same meta_field lat / lng, it doesn’t work.

    Thanks

  12. Florian's profile image.

    Hi there,

    Is there a way to make “orderby => distance” working in a query with multiple post type ?

    In my case when I add more than one post type, the “orderby => distance” is not working.

    OR maybe the problem is not on your plugin ?

    • gschoppe's profile image.

      I’m sorry to say, I don’t really see a reason why multiple post types would effect orderby distance. what result are you seeing? Have you confirmed that things are working for you if you search only for a single post type?

  13. D45's profile image.

    Thank you so much for this, works great for a simple job search site that I am building, using ACF’s Google Maps field to take the address (rather than having multiple fields making it repetitive for the end-user).

    For those that were stuck for a short while like I was, I created two text ACF fields for latitude and longitude. I then created an action to take the lat and long from the ACF Google Maps field and store them in the new lat/long text fields upon saving the post like so:


    function updateJobLatLong($post_id) {
    $location = get_field('location', $post_id);
    if($location):
    $field_key_1 = 'field_5e6c1eac4b1c2';
    $field_key_2 = 'field_5e6c1eb14b1c3';
    $value_1 = $location['lat'];
    $value_2 = $location['lng'];
    update_field($field_key_1, $value_1, $post_id);
    update_field($field_key_2, $value_2, $post_id);
    endif;
    }
    add_action('acf/save_post', 'updateJobLatLong');

    Instead of building a custom search, I just overrode the default search:


    function jobLocationFilter($query) {
    if($query->is_search && !is_admin()):
    $s_location = htmlspecialchars($_GET['location']);
    $s_lat = htmlspecialchars($_GET['lat']);
    $s_long = htmlspecialchars($_GET['long']);
    $s_distance = htmlspecialchars($_GET['distance']);

    if($s_location && $s_lat && $s_long):
    $query->set('geo_query',array(
    'lat_field' => 'latitude',
    'lng_field' => 'longitude',
    'latitude' => $s_lat,
    'longitude' => $s_long,
    'distance' => $s_distance,
    'units' => 'miles'
    ));
    $query->set('orderby','distance');
    $query->set('order','ASC');
    endif;
    endif;
    return $query;
    }
    add_filter('pre_get_posts','jobLocationFilter');

    Works perfectly for me!

Your email address will not be published.

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>