geohash_bounds 0.1.0
geohash_bounds: ^0.1.0 copied to clipboard
Pure-Dart geohash query bounds for Firestore spatial queries, with viewport-to-cell clustering and antimeridian handling.
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.