Pune listening to Hiromi — Spiral · Dota 2 playing Dota 2

5 July 2026 · ChargeDisha · visit project →

Nearby EV chargers in under 100ms, without a spatial database

Why ChargeDisha uses 7-character geohashes on Firestore instead of PostGIS, and how the neighbor-cell trick works.

The core query in ChargeDisha is boring to describe and annoying to implement: “show me charging stations within 10km of where I am.” If I were on Postgres I’d install PostGIS, add a GiST index, and be done. But ChargeDisha runs on Firestore, because the rest of the app (check-ins, gamification, cloud functions) fits Firestore well and I didn’t want to run a second database for one query.

Firestore has no spatial types. What it does have is fast range queries on string fields. That’s enough, if you encode location as a string that preserves proximity. That’s exactly what a geohash is.

How the encoding works

A geohash interleaves the bits of latitude and longitude and encodes the result in base32. The property that matters: strings that share a prefix are geographically close. tekze and tekzf are neighboring cells; tekze and w21zd are different cities.

Each extra character divides the cell roughly 32 ways, so precision is a knob:

5 chars  ≈ 4.9 km  × 4.9 km
6 chars  ≈ 1.2 km  × 0.6 km
7 chars  ≈ 153 m   × 153 m

I store a 7-character geohash on every station document. Seven characters is precise enough that a cell never contains more than a handful of stations, and coarse enough that the index stays cheap.

The query

To find stations within 10km, I don’t query at 7 characters. I truncate to the precision whose cell size covers the radius, then query that cell and its eight neighbors. The neighbors matter: your position can sit right on a cell boundary, and a station 200m away can live in the adjacent cell with a completely different hash prefix.

Each cell lookup is a prefix range query, which Firestore serves straight off the index:

start = cell               # e.g. "tekz"
end   = cell + "~"          # ~ sorts after every base32 char
docs = stations.order_by("geohash").start_at(start).end_at(end)

Nine index-backed range reads, then one pass in the Flask service to compute exact haversine distances and drop anything outside the true radius (the cells cover a square, the user asked for a circle). The whole thing comes back in well under 100ms, which on a mobile connection is indistinguishable from instant.

What I’d tell someone considering this

Use the boring spatial database if you have one. But if your data already lives in a key-value or document store, a geohash column plus the neighbor-cell trick gets you real proximity search with nothing but a sorted index — no new infrastructure, no extension, nothing to operate. For a nights-and-weekends project where I’m the whole ops team, that last part is the feature.

← All writing   ·   Disagree with something? Email me.