Location Clustering

Spatialized founder Jozef Sorocin
Jozef Soročin
Updated 07/13/2025

Given a full-text query of "New York City", I want to cluster the selected locations into buckets that I can render on a map. On top of that, I want to retrieve the bounds of the viewport that my map will "fly to".

The points of interest below are marked red:

Base map courtesy of http://geojson.io/

Base map courtesy of geojson.io

Base map courtesy of http://geojson.io/

We'll be utilizing:

Here's the pseudo-code:

{
  "query": MATCH city
  "aggs": {
    "clusters": {
      AGGREGATE on geohash grid,
      COMPUTE weighted centroid
      }
    },
    "bounds": {
      COMPUTE viewport bounds
    }
  }
}

It's tempting to use a geo_centroid aggregation without the parent geohash_grid but geo_centroid does not allow for adjusting the zoom-based cluster density. But of course, UI clustering only makes sense when we provide the map's current zoom level (→ "precision"):

Now, ES implements the 12 geohash precision levels .

Geo-hashing deserves a chapter of its own but for now, check out this interactive map to gain an intuitive understanding of the geohash mechanism.

Back to the precision levels — these don't directly correspond to the zoom levels you know from Google Maps, Mapbox, or Leaflet, so an adjustment will be needed. Here's the formula that I use to translate a GoogleMaps zoom into the precision parameter:

// only integer values are allowed -- mapbox & leaflet support floats too so keep that in mind
let precision = int(map_zoom - 8);

if (precision < 1) {
  // Default to the highest "zoom-out".
  // Consider the whole earth & expect 1 - 3 clusters in total
  precision = 1;
} else if (precision > 7) {
  // Don't go beyond 7 because we'll get too many 1-member
  // clusters which could be actual markers/pins instead
  precision = 7;
}

// pass the `precision` into the ES request body

Once we've determined the desired precision, we can proceed onto the final query.

First, let's create geo index similar to the one in From the Viewport to the Query

Join 200+ developers who've mastered this! Get Complete Access — €19
Already a member? Sign in here