geohash-dotnet
June 11, 2026 · View on GitHub
Fast, dependency-light geohashing for .NET — encode, decode, neighbors, polygon coverage, radius search, and compression. Handles the ugly parts (antimeridian, poles, date-line wrapping) correctly, with the test suite to prove it.
dotnet add package geohash-dotnet
v3 is a major rewrite — hot paths are allocation-free, polygon hashing is 10–50× faster via prepared geometries, and radius/circle search is new. Developed with the support of Claude Fable 5 (Anthropic), including the discovery and fix of a subtle nearest-point bug near the poles. 🤖
🛝 Try it live in the browser playground → the full library running in your tab via WebAssembly, nothing to install. New to geohashes? Take the 60-second guided tour — the map explains the whole idea by performing it.
30-Second Tour
var geohasher = new Geohasher();
string hash = geohasher.Encode(52.5163, 13.3777, precision: 7); // "u33db2m"
var (lat, lng) = geohasher.Decode("u33db2m"); // cell center
BoundingBox box = geohasher.GetBoundingBox("u33db2m"); // exact cell bounds
string east = geohasher.GetNeighbor("u33db2m", Direction.East); // adjacent cell
Dictionary<Direction, string> all8 = geohasher.GetNeighbors("u33db2m");
string parent = geohasher.GetParent("u33db2m"); // "u33db2" (coarser)
string[] children = geohasher.GetSubhashes("u33db2m"); // 32 finer cells
🎯 Radius Search (new)
Cover a circle with geohash cells — the building block for every "find things near me" feature:
var radiusHasher = new RadiusHasher();
// All precision-7 cells within 2 km of the Brandenburg Gate
var cells = radiusHasher.GetHashes(52.5163, 13.3777, 2_000, geohashPrecision: 7);
// Or let the library pick a sensible precision for the radius
var auto = radiusHasher.GetHashes(52.5163, 13.3777, 2_000);
// Distance helpers included
double meters = RadiusHasher.GetDistanceMeters("u33db2m", "u33dc0k");
int precision = RadiusHasher.GetPrecisionForRadius(150); // → cell size ≈ your radius
Use case: proximity search on a plain database
No PostGIS, no extensions — just a string prefix index:
// "Show restaurants within 1.5 km" against any SQL/NoSQL store:
var cells = radiusHasher.GetHashes(userLat, userLng, 1_500, 6);
// WHERE geohash6 IN (@cells) → candidate rows
// Then rank the few survivors by exact distance:
var nearby = candidates
.Select(r => (r, d: RadiusHasher.GetDistanceMeters(userLat, userLng, r.Lat, r.Lng)))
.Where(x => x.d <= 1_500)
.OrderBy(x => x.d);
Use case: geofence alerting
// Precompute once: cells fully inside the alert zone vs. boundary cells
var inner = radiusHasher.GetHashes(zoneLat, zoneLng, 500, 8, GeohashInclusionCriteria.Contains);
var edge = radiusHasher.GetHashes(zoneLat, zoneLng, 500, 8, GeohashInclusionCriteria.Intersects);
edge.ExceptWith(inner);
// Per GPS ping: O(1) hash lookup, exact math only for boundary cells
string cell = geohasher.Encode(ping.Lat, ping.Lng, 8);
bool inside = inner.Contains(cell)
|| (edge.Contains(cell) && RadiusHasher.GetDistanceMeters(zoneLat, zoneLng, ping.Lat, ping.Lng) <= 500);
Works correctly across the antimeridian and over the poles — a 100 km circle at (89.5°, 0°) knows the shortest path to a cell at 179°E goes across the pole, not around the parallel.
🗺 PolygonHasher
Cover any (NetTopologySuite) polygon — city limits, delivery zones, country borders:
var polygonHasher = new PolygonHasher();
var zone = new Polygon(new LinearRing(new[] {
new Coordinate(-122.4183, 37.7755), new Coordinate(-122.4183, 37.7814),
new Coordinate(-122.4085, 37.7814), new Coordinate(-122.4085, 37.7755),
new Coordinate(-122.4183, 37.7755)
}));
var cells = polygonHasher.GetHashes(zone, 7,
PolygonHasher.GeohashInclusionCriteria.Intersects,
progress: new Progress<double>(p => Console.Write($"\r{p:P0}")),
cancellationToken: ct);
Antimeridian-crossing polygons (Fiji, Chukotka, the Pacific) are split and handled automatically.
🗜 GeohashCompressor
Shrink a covering to the minimal equivalent set — 32 sibling cells collapse into their parent, recursively:
var compressor = new GeohashCompressor();
var cells = polygonHasher.GetHashes(cityPolygon, 8); // e.g. 48,000 cells
var minimal = compressor.Compress(cells); // e.g. 1,900 cells, same area
Perfect before persisting coverings or shipping them to clients.
🤖 For AI Agents & Codegen
This library is deliberately friendly to automated callers:
- Stateless & thread-safe — every class can be a singleton; no setup, no config, no I/O.
- Deterministic — same input, same output, every platform.
- Strict validation — invalid input throws
ArgumentException/ArgumentOutOfRangeExceptionwith actionable messages (including what to change), never silent garbage. - Self-describing API — full XML docs on every member; signatures below.
Geohasher : Encode(lat,lng,precision) | Decode(hash) | GetBoundingBox(hash)
GetNeighbor(hash,dir) | GetNeighbors(hash) | GetParent(hash)
GetSubhashes(hash) | IsValid(hash)
RadiusHasher : GetHashes(lat,lng,radiusM[,precision][,criteria])
GetDistanceMeters(a,b) | GetPrecisionForRadius(radiusM)
GetCellSizeMeters(precision[,lat])
PolygonHasher : GetHashes(polygon,precision[,criteria][,progress][,ct])
GeohashCompressor : Compress(hashes[,minLevel][,maxLevel])
Precision : named constants, e.g. Precision.Size_km_1x1 == 6
⚡ Performance
Encode/Decode: allocation-free hot path (stackalloc + O(1) lookup tables).RadiusHasher: one haversine term per candidate cell, noasin/sqrtin the loop — typical queries complete in microseconds.PolygonHasher: prepared-geometry spatial index + arithmetic cell bounds; parallelized per row. Country-sized polygons at precision 6–7 run in seconds.GeohashCompressor: O(n log n) — sort once, single-pass prune, level-wise merge.
Each precision level multiplies cell count by 32 — choose the coarsest
precision your use case tolerates (Precision constants or
RadiusHasher.GetPrecisionForRadius help).
🛝 Playground
Everything above, interactive: postlagerkarte.github.io/geohash-dotnet — hover to encode live, cover circles and polygons, compress coverings, and copy the exact C# for whatever you just did. The library runs in your browser via WebAssembly; the timings you see are real.
Source in playground/ — run locally with dotnet run --project playground.
License & Credits
MIT. v3 rewrite, radius search, and test suite developed with the support of Claude Fable 5 by Anthropic.
