PyGeoHash v3.3.2: Reliability at the Edges

PyGeoHash v3.3.2 is on PyPI, and it continues the run the last few releases have been on. v3.3.0 made the hot paths fast. v3.3.1 stopped the library crashing or hanging when you handed it something bad. This one tightens the remaining cases where it answered confidently and the answer did not hold up at the edges of the map, and it improves how the package reaches you: ARM64 wheels are built natively now, and every artifact is smoke tested from a clean environment before it ships.

pip install --upgrade pygeohash

Inverted bounding boxes now raise

BoundingBox takes its arguments interleaved, as (min_lat, min_lon, max_lat, max_lon). A reasonable person guesses it groups them instead, as (min_lat, max_lat, min_lon, max_lon). Write it the grouped way and you used to get an object back. No error, no warning, just a box with its minimums above its maximums.

That box then degraded quietly everywhere it went. geohashes_in_box returned an empty list. is_point_in_box returned False for every point you tried. The box did not intersect itself. Each of those is a plausible answer to a sensible question, which is what made the failure so expensive to track down.

That construction now raises a ValueError naming the offending pair and the expected field order. Degenerate boxes where a minimum equals its maximum are still valid. No box that get_bounding_box produces is affected. Boxes written to span the antimeridian are rejected by the same check, which is honest, since the library never supported them.

This is a behavior change in a patch release. The only code it breaks was already returning wrong results on every call, so a loud error is strictly better than what it replaces, and it was not worth making people wait for a minor version to get it.

Three bugs at longitude 180

get_adjacent could not wrap east or west across the antimeridian. Ask for the eastern neighbor of a cell on the far side of the Pacific and you got back a cell that neighbored nothing nearby. It wraps correctly now, and it validates its geohash and direction arguments rather than trusting them.

Mean longitude across a collection was an arithmetic mean. That works until your points straddle the antimeridian, at which point averaging 179 and -179 gives you 0, a location on the opposite side of the planet from every input. It is a circular mean now, so the answer lands where the data is.

Generated Folium grid viewports could be pushed past the world bounds and render a broken map. They are clamped.

Corners, capitals, and identity

geohashes_in_box was dropping cells at the corners of the box. Not the edges, only the corners, which is the kind of off-by-one that survives for years because the output still looks about right.

Decoding accepts uppercase and mixed-case geohashes now. Geohash strings are conventionally lowercase, but they arrive from APIs and spreadsheets in whatever case they arrive in, and rejecting EZS42 while accepting ezs42 was pedantry rather than validation. geohash_approximate_distance got the same treatment, so two identical geohashes in different cases correctly measure zero apart.

The cardinal selectors, northern, southern, eastern, and western, were re-encoding their chosen geohash at precision 12 before returning it. Hand them a five character geohash and you got a twelve character one back, describing a point inside your input rather than your input. They return the string you gave them.

Tighter validation everywhere else

Over-precision geohashes are rejected when decoding. Booleans are rejected as coordinates and precision values in both the Python and the direct C APIs. Bounding boxes reject out-of-world coordinates. The collection statistics API rejects a bare string where it expects a collection. Non-string values in a Pandas geohash Series are caught. The visualization helpers reject empty collections instead of drawing an empty figure.

The logging helpers do what their docstrings always claimed: they emit records at the level you request, and they register under pygeohash.viz rather than pygeohash.pygeohash.viz. Optional modules stay lazy until you touch one of their public attributes, so importing the library costs nothing if you never plot anything.

The dependency-free docstring examples run as doctests in CI, and the cross-library benchmark comparison is published in the docs instead of living in a script nobody runs.

Getting the package

Distribution got attention this cycle too. Linux ARM64 wheels are built natively rather than under emulation, which makes them faster to produce and removes a class of emulation-only build weirdness. Every wheel and sdist is now installed into a clean environment and exercised from outside the checkout before publication, so a broken artifact fails the release rather than reaching PyPI.

That is 54 wheels on this release, covering CPython 3.9 through 3.15 across macOS, Linux, Windows, x86_64 and ARM64.

372 tests pass. No new runtime dependencies. If you pin, ask for it directly:

pip install pygeohash==3.3.2

Twenty-nine commits, mostly not mine

Twenty-nine commits landed between v3.3.1 and this release, and the agent loop that runs my open source work wrote nearly all of them. Each one is a real bug with a test that fails without the fix.

That loop is a good match for this particular kind of release. Finding the corner cells that geohashes_in_box drops means walking every branch of a function you have already read a dozen times, asking a slightly different question each pass. The antimeridian bugs are three instances of one blind spot in three unrelated files, which is exactly the pattern you miss by hand and catch by sweep. I gate the work and merge it myself, but the thing that went looking through the same code for the ninth time was not me.

Full history is in the CHANGELOG, and the project page has the quick start if you are new to the library.