geohash_bounds
Pure-Dart geohash query bounds for spatial range queries — a port of the
geofire-common npm package,
plus viewport helpers for map-based apps.
Works with Firestore or any datastore that supports string range comparisons. Zero dependencies, no Flutter required.
Why this package?
Most geo-query packages bundle a full Firestore streaming layer. This package only does the math: it hands you geohash range bounds and lets you build the queries yourself — with your own converters, caching, and pagination.
It also solves two problems the original geofire-common doesn't:
- Viewport clustering:
cellsForViewportenumerates a bounded number of coarse geohash cells covering the visible map, so at low zoom you can run one cheap aggregationcount()per cell instead of downloading thousands of documents. - Antimeridian handling:
viewportToCircleandwrapLongitudedeal with date-line-crossing viewports and world-copy longitudes (e.g.lng: 200) that map SDKs like Mapbox and Google Maps produce.
Usage
Store a geohash on each document alongside its coordinates:
import 'package:geohash_bounds/geohash_bounds.dart';
final geohash = GeohashUtil.encode(48.1351, 11.5820); // 'u281zd9z2h'
Radius query (Firestore example)
final bounds = GeohashUtil.queryBounds(
centerLat: 48.1351,
centerLng: 11.5820,
radiusInMeters: 5000,
);
final snapshots = await Future.wait(bounds.map((b) {
return FirebaseFirestore.instance
.collection('places')
.orderBy('geohash')
.startAt([b[0]])
.endAt([b[1]])
.get();
}));
// The bounds cover a bounding box, not an exact circle — filter false
// positives by distance:
final results = snapshots
.expand((s) => s.docs)
.where((doc) {
final d = doc.data();
return GeohashUtil.distanceBetween(
48.1351, 11.5820, d['lat'], d['lng'],
) <=
5000;
})
.toList();
Query the visible map area
final circle = GeohashUtil.viewportToCircle(
neLat: bounds.northeast.latitude,
neLng: bounds.northeast.longitude,
swLat: bounds.southwest.latitude,
swLng: bounds.southwest.longitude,
);
final queryBounds = GeohashUtil.queryBounds(
centerLat: circle.centerLat,
centerLng: circle.centerLng,
radiusInMeters: circle.radiusMeters,
);
Cheap clusters at low zoom
When the user zooms out, fetching every document gets expensive. Instead,
split the viewport into at most maxCells geohash cells and run one
aggregation count per cell:
final cells = GeohashUtil.cellsForViewport(
neLat: 55, neLng: 15,
swLat: 45, swLng: 5,
maxCells: 30,
);
for (final cell in cells) {
final count = await FirebaseFirestore.instance
.collection('places')
.where('geohash', isGreaterThanOrEqualTo: cell.prefix)
.where('geohash', isLessThanOrEqualTo: '${cell.prefix}~')
.count()
.get();
// Place a cluster marker at (cell.centerLat, cell.centerLng)
// showing count.count.
}
cellsForViewport picks the finest geohash precision whose grid still fits
within maxCells, so the number of queries stays bounded at every zoom level.
API
| Method | Description |
|---|---|
encode(lat, lng, {precision}) |
Geohash string for a coordinate |
queryBounds({centerLat, centerLng, radiusInMeters}) |
[start, end] hash pairs covering a circle |
cellsForViewport({neLat, neLng, swLat, swLng, maxCells, maxPrefixLength}) |
Coarse cells covering a viewport, with cell centers |
viewportToCircle({neLat, neLng, swLat, swLng}) |
Viewport corners → center + radius, antimeridian-safe |
distanceBetween(lat1, lng1, lat2, lng2) |
Haversine distance in meters |
wrapLongitude(lng) |
Normalize longitude into [-180, 180] |
Additional information
Battle-tested in Pin Your Plate, a food-mapping app querying pins worldwide. Issues and contributions welcome on GitHub.
Libraries
- geohash_bounds
- Pure-Dart geohash query bounds for spatial range queries.