Find results near a specified location

FacetWP Proximity facet - filter by distance to locationThe Proximity facet lets you find results near a specified location, entered manually, or by clicking the “Locate me” icon.

After entering a location, a radius can be chosen from a pre-defined list, with a dropdown or slider. Results within that radius from the set location are then displayed.

How it works

After entering a location, FacetWP uses Google to grab the latitude, longitude of that location’s central point. Results within the user-defined radius of that point are displayed.

Proximity facet radius and countries / regions
A Proximity search for the region “Lombardy, Italy” (with the red border and blue marker at its geographical center). When the radius is set to 40km, it does not return any results in its capital Milan.

It does not understand geographic / political boundaries. If you search for a country (e.g. “Italy”), the facet will return results extending from that country’s geographical center point (as defined by Google), which may include results from other countries if the chosen radius is large enough.

Be aware that this behavior is possibly confusing for users. For example, if a user searches for the region Lombardy (Italy), they may expect to see results within the whole region (including its capital Milan). They may even expect results with a radius extending from the border of the region, not the center point. The Lombardy region is about 220km from west to east. So if a user searches it with a radius set to 40km, the facet will only display results within a circle of 80km diameter around Lombardy’s center point, which covers only a small part of the region, and does not include its capital Milan.

Keep this behavior in mind when choosing radius options for your situation. If you expect users to often search for regions or even countries, it makes sense to offer higher radius settings that cover whole regions or countries. Also, setting the default radius to a higher value can make the initial results more intuitively correct.

Another approach would be to limit what “place types” are returned by the autocomplete results, and exclude regions and countries. And/or to limit results to a specific country.

Combine a Proximity facet with a Map facet

If you use a Proximity facet on its own, it will return results within the chosen radius from the specified location. But there is no map for the user to interact with, nor any other feedback that includes distances.

You can show the resulting posts’ distances to the specified location, and/or let users sort results by distance. But to give users visual feedback, a Proximity facet can very well be combined with a Map facet.

When a location and radius are set (or changed) with the Proximity facet, the map will automatically zoom in or out, showing the locations that are within the radius of the set location. It will also display a marker pin at the set location (which can be customized or disabled).

Available options

Name Description
Data source The Data source should be a custom field containing a comma-separated latitude, longitude. Or – if you use a separate custom field for the longitude – only a latitude. See below for more info.
Longitude Choose a custom field to use for the longitude value, if stored separately from the latitude (optional). See below for more info.
Unit of measurement Miles or Kilometers
Radius UI The UI element to use for the radius selector (Dropdown, Slider or None)
Radius options A comma-separated list of radius choices. The values need to be integers. Default: “10, 25, 50, 100, 250”. These radius choices can be changed programmatically if needed.
Default radius The default radius to be shown in the radius selector. If left empty, the first radius option will be the default if the Radius UI is set to Dropdown, and the middle of the range will be the default if the Radius UI is set to Slider.

Data source

The Data source should be a custom field containing a comma-separated latitude, longitude.

You can also use separate custom fields for the latitude and longitude, see the available options.

Note: it is possible to index multiple locations for a post, by using a multi-value custom field, like a repeater field or for example a simple checkboxes/dropdown field.

Other data sources

The following plugins and themes offer dedicated custom fields for latitude and longitude that can be used as data source for the Proximity facet. Click the links for specific instructions on using these fields:

Google Maps API key

Generate a Google Maps API key

The Proximity facet requires a valid Google Maps API key. To generate a key, you have to log into Google Cloud Console, set up a project, add an API key under API's & Services > Credentials, and enable the following three API services for the key:

  1. Maps JavaScript API: required for the Proximity / Map facets to work
  2. Geocoding API: required for the Proximity facet’s “Locate me” button
  3. Places API: required for the Proximity facet’s autocomplete box

To be able to use the key, you also need to set up a Billing Account in Google Cloud Console, and add your project to it. You will need a creditcard to set up billing. There is a free monthly credit of $200, which amounts to 28000+ map loads per month.

Add the Google Maps API key to FacetWP

After generating an API key, add it to: Settings > FacetWP > Settings > Google Maps API key.
Both the Proximity facet and the Map facet use this key.

Add the Google Maps API key to Advanced Custom Fields

If you are using the Advanced Custom Fields Google Map field as the data source, make sure you pass the same Google Maps API key into ACF. ACF needs the Google Maps API in the backend to geolocate the address entered for each post.

Add the Google Maps API key to Listify, Listable, WP Job Manager

If you are using Listify theme, Listable theme and/or the WP Job Manager plugin, make sure to also add the Google Maps API key to:
(Job) Listings > Settings > Google Maps API Key

The Listify theme itself has a second settings to enter the Google Maps API key, at:
Appearance > Customize > Listings > Map settings

Accuracy of the “Locate me” button

When the “Locate me” button is pressed, FacetWP attempts to determine the user’s location. FacetWP first makes a browser Geolocation API request, which returns an approximate latitude/longitude (based on the user’s IP address).

Then FacetWP performs a reverse lookup to determine the human-friendly physical address. This not an exact science, and occasionally the location can be off by several kilometers.

Limit autocomplete results to a specific country

It is possible to limit the autocomplete results to one or more countries with the “componentRestrictions” option. You can specify a single country, or an array of up to five country code strings. The country code needs to be a two-letter ISO 3166-1 Alpha-2 country code and is case insensitive.

This code example forces the autocomplete to show only choices in Germany (country code = de):

<?php
add_filter( 'facetwp_proximity_autocomplete_options', function( $options ) {
    $options['componentRestrictions'] = [
        'country' => ['de'],
    ];
    return $options;
});

Limit autocomplete results to specific place types

It is possible to limit the autocomplete results to certain “place types”, like countries, regions, cities, municipalities or postal codes. A list of available place types can be found in the Places API reference.

This code example limits autocomplete results to cities only:

<?php
add_filter( 'facetwp_proximity_autocomplete_options', function( $options ) {
    $options['types'] = ['(cities)'];
    return $options;
});

Bias autocomplete results towards a specific area

It is possible to bias the autocomplete prediction results towards a specific area by setting a location with lat/lng coordinates, and defining a “bounding box” surrounding that location.

The following example sets New York City as location and defines a bounding box with sides of approximately 10km away from the specified center.

The results will biased towards, but not restricted to to places within the bounding box. Unfortunately, restricting the results is currently not possible with the available options the Google Places API provides (but it could very well become possible in the future).

You can test this example code by entering “Amsterdam” in the Proximity facet’s field. The first prediction result will be a billiards club in New York with that name, and not the capital of the Netherlands, which would otherwise be the first.

<?php
add_filter( 'facetwp_proximity_autocomplete_options', function( $options ) {

  // New York City. Test results with "Amsterdam"
  $mylocation = [ 'lat' => 40.6974034, 'lng' => -74.1197615 ];

  // Create a bounding box with sides ~10km away from the location point
  $myBounds = [
    'north' => $mylocation['lat'] + 0.1,
    'south' => $mylocation['lat'] - 0.1,
    'east'  => $mylocation['lng'] + 0.1,
    'west'  => $mylocation['lng'] - 0.1,
  ];

  $options['bounds'] = $myBounds;

  return $options;
} );

Change the minimum character length and delay

This code example limits the minimum characters to 3 and sets the delay to 250 ms:

<?php
add_filter( 'facetwp_assets', function( $assets ) {
    FWP()->display->json['proximity']['queryDelay'] = 250; // default = 250 milliseconds
    FWP()->display->json['proximity']['minLength'] = 3; // default = 3 characters
    return $assets;
} );

Display the post distance

When using a Proximity facet, you can use the facetwp_get_distance() function to get each post’s distance from the user-specified location. If the facet is in use as a filter (a location is entered in the facet field), the function will return the numeric distance, or FALSE otherwise.

The distance is calculated in the units specified in the Proximity facet’s “Unit of measurement” setting (miles or kilometers). Be aware that the distance output is a float value, which needs to be rounded before being displayed.

In the loop you can use it like this:

// Get the distance as a float value
$distance = facetwp_get_distance();

// Round distance to 2 decimals and append ' mi'
if ( false !== $distance ) {
    echo round( $distance, 2 ) . ' mi';
}

To get the distance of a specific post only, you can pass a post_id:

// Get the distance as a float value
$distance = facetwp_get_distance( $post_id );

// Round distance to 2 decimals and append ' km'
if ( false !== $distance ) {
    echo round( $distance, 2 ) . ' km'; 
}

If your posts have multiple locations, the facetwp_get_distance() function will return the distance of the location that is closest to the user-specified location. And if you need to know which one that is, you can let FacetWP store that location’s lat/lng values, and retrieve them.

Customize the post distance output

If you need to customize the output of the facetwp_get_distance() function, for example doing some calculations, rounding, or prepending/appending a string, you can use the following hook. Be aware that the passed $distance is a float value:

<?php
add_filter( 'facetwp_proximity_distance_output', function( $distance ) {
  $distance = "Distance: " . round( $distance,2 ) + 5.5 . ' km';
  return $distance;
}, 10, 2 );

Store and get the (closest) post location

Add the following hook to your (child) theme’s functions.php to let FacetWP dynamically store the lat and lng values of each post in the filtered listing:

add_filter( 'facetwp_proximity_store_latlng', '__return_true' );

With this hook in place, and the Proximity facet in use as a filter, you can dynamically get the lat and lng values for a specific post (or for each post in the listing’s loop), as follows:

$latlngposts = FWP()->facet->facet_types['proximity']->post_latlng;
if ( ! empty( $latlngposts ) ) {
  $latlng = $latlngposts[THE_POST_ID]; // Replace "THE_POST_ID" with the post id. If used in the loop, you can use: get_the_ID()
  $lat = $latlng[0];
  $lng = $latlng[1];
  // do something with $lat and $lng
}

If your posts have multiple locations that are indexed by FacetWP, only one location is dynamically stored: the one that is closest to the user-specified location. This is the same location that is used by the facetwp_get_distance() function to calculate the post’s distance from the user-specified location.

Sort results by distance

When a Proximity facet is in use as a filter (with a location entered), the post IDs are automatically re-sorted in the post__in argument of the query, from nearest to furthest. (See below for how to prevent that).

This makes it possible to sort results by distance with the following code. This order is retained until a Sort facet is also used, which then overrules the sort order. Make sure to replace yoursortfacet with the name of your Sort facet:

<?php
add_action( 'pre_get_posts', function( $query ) {
  if ( ! class_exists( 'FacetWP_Helper' ) ) {
    return;
  }

  $facets_in_use = FWP()->facet->facets;
  $prefix        = FWP()->helper->get_setting( 'prefix' );

  // replace  'yoursortfacet' with the name of your sort facet
  $using_sort    = isset( FWP()->facet->http_params['get'][ $prefix . 'yoursortfacet' ] ); 

  $is_main_query = false;
  if ( is_array( FWP()->facet->template ) ) {
    if ( 'wp' != FWP()->facet->template['name'] || true === $query->get( 'facetwp' ) ) {
      $is_main_query = true;
    }
  }

  if ( ! empty( $facets_in_use ) && ! $using_sort && $is_main_query ) {
    foreach ( $facets_in_use as $f ) {
      if ( 'proximity' == $f['type'] && ! empty( $f['selected_values'] ) ) {
        $query->set( 'orderby', array( 'post__in' => 'ASC' ) ); // or 'DESC'
      }
    }
  }
}, 1000 );

Sort results by distance with a Sort facet

Sort by distance
Make sure sort options have unique names.

If you want to allow users to sort by distance as a sorting option in a Sort facet, create a new sort option and set its data source to post__in with ASC or DESC order.

If you make both a ASC and a DESC sort option, make sure each sort option has a unique technical name set.

Be aware that sorting by distance only works when a location has been entered in the Proximity facet.

Prevent ordering by distance of post__in argument

If you want to prevent the Proximity facet from re-ordering the post IDs within the post__in argument of the query, add the the following code to your (child) theme’s functions.php:

<?php
add_filter( 'facetwp_query_args', function ( $query_args ) {
	remove_filter( 'facetwp_filtered_post_ids', [ FWP()->helper->facet_types['proximity'], 'sort_by_distance' ] );
	return $query_args;
} );

Change the radius options programmatically

If you want to change the radius options programmatically, add the following code to your (child) theme’s functions.php. The first value will be the default radius:

add_filter( 'facetwp_proximity_radius_options', function( $radius_options ) {
  return array( 250, 500, 1000, 2500 ); // The first value is the default value
}, 10, 2 );

See also