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

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( "
    SELECT DISTINCT
        p.ID,
        p.post_title,
        map_lat.meta_value,
        map_lng.meta_value,
        ( %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",
    $earth_radius,
    $latitude,
    $longitude,
    $latitude,
    $post_id,
    $distance,
    $limit
  );

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

  if ( $neighbors ) {
    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

        wp_reset_postdata();
      }
    }

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

  die();
}
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

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) {
    console.error(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.


postNeighbors(ajax_neighbors_object.location)
  .then(({data})=>doYourThing(data));

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


getUserLocation()
  .then(postNeighbors)
  .then(({data})=>doYourThing(data));

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.

2 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!

Leave a Reply