Querying Nearby Locations in WordPress using Geo Coordinates

Campus Arrival nearby schools

In the course of developing Campus Arrival, a WordPress site that provides school-specific packing lists, we added a feature to suggest nearby universities. When viewing a school’s checklist, this feature displays other, nearby colleges. When viewing the front page, it suggests colleges near the visitor’s own location. For example, check out the suggestions for MIT, UCLA, or your own location.

Campus Arrival nearby schools

This was implemented in WordPress by querying nearby schools based on geo coordinates, i.e., latitude and longitude. (Our first pass used a custom taxonomy with in-state schools, but we knew the results could be improved.)

If you’d like to do something similar, follow the steps in this guide.

Post Meta

Start by adding custom post meta fields for latitude and longitude. The WP plugin Advanced Custom Fields is a godsend for creating nice admin-facing interfaces to manage custom post meta.

The Hooks

First we start with the PHP code. The meat of the implementation is this function, cobbled together from this comment on the WordPress StackExchange, that accepts a set of coordinates, along with a few other parameters like the max search distance and number of posts to return, and returns the posts with the nearest latitude and longitude.

function query_neighbors( $latitude, $longitude, $post_id = -1, $distance = 400, $limit = 6 ) {
  global $wpdb;
  $earth_radius = 3959; // miles

  $sql = $wpdb->prepare( "
        ( %d * acos(
        cos( radians( %s ) )
        * cos( radians( map_lat.meta_value ) )
        * cos( radians( map_lng.meta_value ) - radians( %s ) )
        + sin( radians( %s ) )
        * sin( radians( map_lat.meta_value ) )
        ) )
        AS distance
    FROM $wpdb->posts p
    INNER JOIN $wpdb->postmeta map_lat ON p.ID = map_lat.post_id
    INNER JOIN $wpdb->postmeta map_lng ON p.ID = map_lng.post_id
    WHERE p.post_type = 'school'
        AND p.post_status = 'publish'
        AND p.ID != %d
        AND map_lat.meta_key = 'latitude'
        AND map_lng.meta_key = 'longitude'
    HAVING distance < %s
    ORDER BY distance ASC
    LIMIT %d",

  $neighbors = $wpdb->get_results( $sql );

  return $neighbors;

This function, along with the AJAX hook below, should be added to your custom plugin or functions.php. In the “do your thing” section, construct your markup, perhaps using a template.

function process_neighbors() {
  check_ajax_referer( 'neighbors_validation', 'security');
  $location = $_POST['location'];
  $url = wp_get_referer();
  $post_id = url_to_postid( $url );

  if ( isset( $location ) ) {
    $neighbors = query_neighbors( $location['latitude'], $location['longitude'], $post_id, 400, 6 );
    $response = '';

    if ( $neighbors ) {
      foreach( $neighbors as $neighbor ) {
        setup_postdata( $neighbor );

        // do your thing


    if ( strlen( trim( $response ) ) > 0 ) {
      wp_send_json_success( $response );
    } else {

add_action( 'wp_ajax_nopriv_post_neighbors', 'process_neighbors' );
add_action( 'wp_ajax_post_neighbors', 'process_neighbors' );

Next, enqueue the JS script that you’ll be creating in the next section.

function load_custom_js() {
  $post_id = get_the_ID();
  $location = array(
    'latitude' => get_post_meta( $post_id, 'latitude', true ),
    'longitude' => get_post_meta( $post_id, 'longitude', true ),
    'post_id' => $post_id

  $url = plugins_url( 'scripts/neighbors.js', __FILE__ );

  wp_enqueue_script( 'neighbors_js', $url, array( 'jquery' ), '', true );
  wp_localize_script( 'neighbors_js', 'ajax_neighbors_object', array(
    'ajax_url' => admin_url( 'admin-ajax.php' ),
    'nonce' => wp_create_nonce( 'neighbors_validation' ),
    'location' => $location
add_action( 'wp_enqueue_scripts', 'load_custom_js' );

The Script

Now we write the JavaScript code that makes the AJAX call to the PHP code to find nearby neighbors, then formats the results and appends them to the current page.

Start with a function that takes a location and sends a POST request to the hook above.

async function postNeighbors(location) {
  let response;

  try {
    response = await $.ajax({
      type: 'POST',
      dataType: 'json',
      url: ajax_neighbors_object.ajax_url,
      data: {action: 'post_neighbors', security: ajax_neighbors_object.nonce, location: location}

    return response;
  } catch (error) {

To compare with another school, call postNeighbors like so. In the doYourThing function, you’ll handle the markup you got in your response, perhaps injecting it into the page and sliding it into view.


To compare with the user’s own location, you just need to add one more step, making use of the handy WordPress geo API.


async function getUserLocation() {
  const response = await fetch('https://public-api.wordpress.com/geo/');
  const json = await response.json();

  return json;

Check out Campus Arrival to see all this in action.

6 comments Write a comment

  1. This looks really cool…I’m diving in. I have a mountain bike website that uses Custom Post Types with coordinates for a pin. Hopefully, I can build out a ‘you may also like’ with this help. Thank you!

  2. Hey, looks exactly what i need but all i get is -1 on my page, i am calling function in a shortcode.

    add_shortcode( ‘nearby’, ‘process_neighbors’);

    Can you explain how can i use shortcode to display it? i don’t see anything in network XHR, what am i missing?

  3. This looks like it may start to get me where I need to go. Does all of this go in functions? How do I call it on, say, my search page?

    • The hooks PHP code can go into functions.php. The JS code can be saved into an external script file, for example neighbors.js, and will be included on all pages by the wp_enqueue_script call.

      You can add conditions to this call to have it included only on search: if( is_search() )

  4. Any idea how to do that while both lat and long are saved in one field? E.g. the Google Maps field of ACF Pro. I know that it is saved as a serialized array, but have no idea how to select them.

Leave a Reply