Geospatial search

In this tutorial, you’ll use Fauna to run geospatial searches in a given area using a bounding box search pattern. The method uses latitude and longitude coordinates to find places in a radius of a provided location. The tutorial uses example locations from Toronto, Canada.

Create the calculateBoundingBox() function

To start, create a user-defined function (UDF) to calculate the minimum and maximum coordinates for the bounding box:

function calculateBoundingBox(
  lat: Number,
  lon: Number,
  radius: Number):{
    minLat: Number,
    maxLat: Number,
    minLon: Number,
    maxLon: Number
  }
{
  let R = 6371
  let radLat = lat * Math.PI / 180
  let deltaLat = radius / R
  let deltaLon = Math.asin(Math.sin(deltaLat) / Math.cos(radLat))
  let minLat = lat - deltaLat * 180 / Math.PI
  let maxLat = lat + deltaLat * 180 / Math.PI
  let minLon = lon - deltaLon * 180 / Math.PI
  let maxLon = lon + deltaLon * 180 / Math.PI
  let cord = {
    minLat: minLat,
    maxLat: maxLat,
    minLon: minLon,
    maxLon: maxLon
  }
  cord
}

Create the haversine() function

Next, create a haversine() function to calculate the distance, in kilometers, between two geographic points using their coordinates. The function uses the Haversine formula to account for the curvature of the earth.

function haversine(
  coords1: { latitude: Number, longitude: Number },
  coords2: { latitude: Number, longitude: Number }): Number
{
  let lat1 = coords1.latitude
  let lon1 = coords1.longitude
  let lat2 = coords2.latitude
  let lon2 = coords2.longitude
  let R = 6371
  let x1 = lat2 - lat1
  let dLat = x1 * Math.PI / 180
  let x2 = lon2 - lon1
  let dLon = x2 * Math.PI / 180
  let a =
    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(lat1 * Math.PI / 180) *
    Math.cos(lat2 * Math.PI / 180) *
    Math.sin(dLon / 2) *
    Math.sin(dLon / 2)

  let c = 2 * Math.asin(Math.sqrt(a));
  let distance = R * c

  distance
}

Add sample data

Create a Location collection:

collection Location {
  history_days 0
  index orderByLatLong {
    values [.latitude, .longitude]
  }
}

Add sample data for a few locations to the collection:

Location.create({
  name: "St. Lawrence Market",
  latitude: 43.6487,
  longitude: -79.3716
})

Location.create({
  name: "Royal Ontario Museum",
  latitude: 43.6677,
  longitude: -79.3948
})

Location.create({
  name: "Toronto Music Garden",
  latitude: 43.6366,
  longitude: -79.3951
})

Location.create({
  name: "High Park",
  latitude: 43.6465,
  longitude: -79.4637
})

Location.create({
  name: "Ontario Science Centre",
  latitude: 43.7163,
  longitude: -79.3390
})

Location.create({
  name: "Casa Loma",
  latitude: 43.6780,
  longitude: -79.4094
})

Location.create({
  name: "Scarborough Bluffs",
  latitude: 43.7064,
  longitude: -79.2328
})

Location.create({
  name: "Black Creek Pioneer Village",
  latitude: 43.7735,
  longitude: -79.5164
})

Query inside a radius

Run the following query to search for places in a 5 km radius of a defined location. The results only include places in the radius. Results are ordered by distance from the location.

let distanceKm = 5.0 // find all points within 5 km of the current location

// Define the current location
let latitude = 43.6532
let longitude = -79.3832

let boundingbox = calculateBoundingBox(latitude, longitude, distanceKm)

Location.orderByLatLong({ 
  from: [boundingbox.minLat, boundingbox.minLon], 
  to: [boundingbox.maxLat, boundingbox.maxLon]
})
.where(
  .longitude >= boundingbox.minLon && .longitude <= boundingbox.maxLon
)
.map(l => {
  location: l, 
  distance: haversine(
    { latitude: l.latitude, longitude: l.longitude }, 
    { latitude: latitude, longitude: longitude }
  )
})
.order(asc(.distance))
{
  data: [
    {
      location: {
        id: "396567265944797257",
        coll: Location,
        ts: Time("2024-04-30T05:27:46.260Z"),
        name: "St. Lawrence Market",
        latitude: 43.6487,
        longitude: -79.3716
      },
      distance: 1.0589651226108578
    },
    {
      location: {
        id: "396631857567891533",
        coll: Location,
        ts: Time("2024-04-30T22:34:25.630Z"),
        name: "Royal Ontario Museum",
        latitude: 43.6677,
        longitude: -79.3948
      },
      distance: 1.8628877543323947
    },
    {
      location: {
        id: "396567265945845833",
        coll: Location,
        ts: Time("2024-04-30T05:27:46.260Z"),
        name: "Toronto Music Garden",
        latitude: 43.6366,
        longitude: -79.3951
      },
      distance: 2.0794133933697307
    },
    {
      location: {
        id: "396567265946894409",
        coll: Location,
        ts: Time("2024-04-30T05:27:46.260Z"),
        name: "Casa Loma",
        latitude: 43.678,
        longitude: -79.4094
      },
      distance: 3.470709059384686
    }
  ]
}

Optimizing the query

You can optimize the query in previous example. You can make a cheap index by chunking latitude and longitude together. Update your Location collection to include the following index:

collection Location {
  history_days 0

  compute lat_floor = doc => Math.floor(doc.latitude)

  index by_latitude__logitude_asc {
    terms [.lat_floor]
    values [.longitude]
  }
}

Update the query to use the new index:

// find all points within 5 km of the current location
let distanceKm = 5.0 
// Define the current location
let latitude = 43.6532
let longitude = -79.3832

let boundingbox = calculateBoundingBox(latitude, longitude, distanceKm)

let lat_range = 
  Set.sequence(Math.floor(boundingbox.minLat), Math.ceil(boundingbox.maxLat))

lat_range
  .flatMap(lat => {
    Location.by_latitude__logitude_asc(
      lat * 1.0, 
      { from: boundingbox.minLon, to: boundingbox.maxLon }
    )
  })
  .map(l => {
    location: l, 
    distance: haversine(
      { latitude: l.latitude, longitude: l.longitude }, 
      { latitude: latitude, longitude: longitude }
    )
  })
  .where(.distance < distanceKm)
  .order(asc(.distance))
{
  data: [
    {
      location: {
        id: "396567265944797257",
        coll: Location,
        ts: Time("2024-04-30T05:27:46.260Z"),
        chunk: 42920.0,
        name: "St. Lawrence Market",
        latitude: 43.6487,
        longitude: -79.3716
      },
      distance: 1.0589651226108578
    },
    {
      location: {
        id: "396631857567891533",
        coll: Location,
        ts: Time("2024-04-30T22:34:25.630Z"),
        chunk: 42920.0,
        name: "Royal Ontario Museum",
        latitude: 43.6677,
        longitude: -79.3948
      },
      distance: 1.8628877543323947
    },
    {
      location: {
        id: "396567265945845833",
        coll: Location,
        ts: Time("2024-04-30T05:27:46.260Z"),
        chunk: 42920.0,
        name: "Toronto Music Garden",
        latitude: 43.6366,
        longitude: -79.3951
      },
      distance: 2.0794133933697307
    },
    {
      location: {
        id: "396567265946894409",
        coll: Location,
        ts: Time("2024-04-30T05:27:46.260Z"),
        chunk: 42920.0,
        name: "Casa Loma",
        latitude: 43.678,
        longitude: -79.4094
      },
      distance: 3.470709059384686
    }
  ]
}

Is this article helpful? 

Tell Fauna how the article can be improved:
Visit Fauna's forums or email docs@fauna.com

Thank you for your feedback!