Generating Anti-Meridian and Polar Test Fixtures
Two edge cases break more spatial pipelines than any others: geometry that crosses the ±180° anti-meridian, and features near the poles where Web Mercator is undefined. This guide sits beneath edge case spatial data creation and shows how to generate both as reproducible fixtures, and how to write the assertions that prove a pipeline handles them rather than silently mangling them. These belong in every suite’s fast tier because they are cheap to check and catastrophic to miss — an anti-meridian bug inverts a bounding box and an unhandled polar feature produces infinite coordinates, and both pass naïve validity checks.
Why these two cases break pipelines
The anti-meridian is where longitude wraps from +180° to −180°. A geometry spanning it — a shipping route across the Pacific, a country like Fiji — has vertices with opposite-sign longitudes, so a naïve bounding box spans nearly the whole globe and a naïve centroid lands on the wrong side of the planet. The poles break the other way: Web Mercator (EPSG:3857) is mathematically undefined at ±90° and distorts severely above ~85°, so projecting a polar feature to it yields coordinates that grow without bound. Neither defect is a geometry validity failure — the rings are fine — so only a fixture that deliberately exercises the case will surface the bug.
The bounding box is where both cases break
Neither the anti-meridian nor the pole breaks geometry directly — both break the bounding box, and everything downstream of a bounding box inherits the failure. Seeing that clearly explains why a feature can be perfectly valid and still vanish from a query, land on the wrong tile, or draw a stripe across the whole map.
The polar case damages the same structure from the other axis. Latitude has no wrap, so the box stays finite, but Web Mercator is undefined at the poles and is conventionally truncated near 85°. A feature above that latitude has a box that cannot be expressed in the projection at all, so tools either clamp it silently — moving the feature — or produce an infinite coordinate that propagates as NaN through every subsequent calculation. Both failures happen before any predicate runs, which is why neither is caught by a validity check.
Edge-case fixture reference
| Case | Fixture to keep | Assertion that proves handling |
|---|---|---|
| Anti-meridian polygon | Polygon spanning ±180° | Split geometry has two parts, bbox not global |
| Anti-meridian line | LineString crossing ±180° | Great-circle length is finite and short |
| Polar point | Feature above 85° latitude | Reproject to polar stereographic, coords finite |
| Pole itself | Point at 90° latitude | Web Mercator projection rejected, not inf |
| Dateline-adjacent | Feature at ±179.99° | No accidental wrap |
Step-by-step implementation
The pattern targets Shapely 2.x, GeoPandas 0.14+ and pyproj, generating each fixture and the assertion that validates it.
Step 1 — Generate an anti-meridian crossing
A polygon that spans the dateline, kept in WGS84, is the canonical fixture.
from shapely.geometry import Polygon
import geopandas as gpd
# A box straddling +180 / -180 near Fiji
antimeridian = gpd.GeoDataFrame(
{"id": [1]},
geometry=[Polygon([(179, -18), (-179, -18), (-179, -16), (179, -16), (179, -18)])],
crs="EPSG:4326",
)
Step 2 — Assert the pipeline splits rather than wraps
A correct handler splits the geometry at the dateline (e.g. via a densify-and-split step) so each part stays on one side and the bounding box is local, not global.
def test_antimeridian_bbox_is_not_global(antimeridian):
minx, _, maxx, _ = antimeridian.total_bounds
# A naïve wrap makes width ~358°; a correct split keeps each part narrow.
assert (maxx - minx) < 5 or (maxx - minx) > 355, "ambiguous wrap — split before use"
Step 3 — Generate a polar fixture
from shapely.geometry import Point
polar = gpd.GeoDataFrame(
{"id": [1]}, geometry=[Point(0, 88)], crs="EPSG:4326" # 88° N
)
Step 4 — Assert polar features use an appropriate CRS
Reproject the polar fixture to a polar stereographic CRS (EPSG:3995) and confirm finite coordinates; assert that a Web Mercator projection of the pole itself is rejected, not silently infinite.
import numpy as np
def test_polar_uses_stereographic(polar):
stereo = polar.to_crs(3995) # Arctic polar stereographic
xy = np.array([(p.x, p.y) for p in stereo.geometry])
assert np.isfinite(xy).all(), "polar coords must be finite in a polar CRS"
The choice of a metric, feature-local CRS before measuring is the same rule the spatial tolerance thresholds work applies to high-latitude distortion.
What correct handling actually looks like
There is no single “fix” for either case, because the right behaviour depends on what the pipeline does next. What a test can pin down is that the pipeline made a deliberate choice rather than falling into the default.
For the anti-meridian there are three defensible strategies, and the suite should assert whichever one the pipeline claims. Splitting at the date line turns the feature into a MultiPolygon with two parts, which every renderer and index handles correctly at the cost of changing the geometry type. Shifting to a continuous longitude range — allowing values beyond 180° — keeps a single part and works well for regional data that never wraps twice, but breaks any consumer that validates longitude bounds. Reprojecting into a CRS centred on the region sidesteps the problem entirely and is the right answer when the data is regional rather than global.
| Strategy | Geometry type | Works with a standard index | Breaks when |
|---|---|---|---|
| Split at ±180° | becomes multi-part | yes | a consumer requires single-part geometry |
| Shift longitudes past 180° | unchanged | no — box still spans oddly | a consumer validates the longitude range |
| Reproject to a regional CRS | unchanged | yes | the dataset is genuinely global |
| Do nothing | unchanged | no | always — this is the default, and it is wrong |
For the polar case the equivalent decision is simpler but no less deliberate: either the data stays out of a Mercator-family projection entirely, using a polar stereographic CRS where the pole is an ordinary point, or the pipeline declares a maximum latitude and rejects features above it. What must not happen is silent clamping, and that is the specific behaviour the fixture exists to detect — generate a feature at 87° north, run it through the pipeline, and assert that its latitude either survived unchanged or produced an explicit rejection. A coordinate that quietly came back as 85° is the failure.
Verification pattern
Confirm the fixtures provoke the bug they target on a naïve handler, so the fixture is proven to have teeth.
# A naïve centroid of the anti-meridian fixture lands near 0° longitude — wrong.
c = antimeridian.geometry.iloc[0].centroid
assert abs(c.x) < 1, "naïve centroid demonstrates the wrap bug the fixture must catch"
Failure modes and edge cases
- Bounding-box operations before splitting. Any bbox, centroid or buffer on an unsplit anti-meridian geometry is wrong; split at the dateline first.
- Web Mercator at the pole. Projecting ±90° to EPSG:3857 yields infinite y; guard with a latitude check and use a polar CRS.
- Fixtures stored reprojected. Saving the anti-meridian fixture already split hides the raw-crossing case; keep the unsplit WGS84 form as the fixture.
- ±179.99° false positives. A near-dateline feature that does not actually cross should not be split; test both crossing and adjacent cases.
- Antarctic vs Arctic CRS. EPSG:3995 is Arctic; a southern polar fixture needs EPSG:3031 — pick the hemisphere-correct CRS.
Where these fixtures belong in the suite
Both cases are cheap to generate and cheap to run, so they belong in the fast pre-merge tier rather than in a nightly job. That placement matters because the code paths they exercise — bounding-box construction, tile selection, centroid computation — are touched by almost every change to a spatial pipeline, and a defect introduced there is far cheaper to catch on the commit than in a delivery.
One caveat: keep them out of any test that also asserts on performance or volume. Their value is entirely in the geometry of the edge, so pairing them with a large dataset adds runtime without adding coverage, and it makes the failure harder to read when it comes.
Conclusion
Anti-meridian and polar fixtures are cheap to generate and essential to keep, because the bugs they expose — inverted bounding boxes and infinite polar coordinates — pass every validity check and only surface against data that deliberately crosses the dateline or approaches the pole. Keep both in the fast tier with assertions that prove the pipeline splits and reprojects correctly. For the wider edge-case catalogue, return to edge case spatial data creation.
Keep both fixtures in version control as generated artefacts with their seeds recorded, so a failure can be reproduced exactly rather than approximately.
Both fixtures are also useful as a smoke test for a new dependency: running them after a GDAL, PROJ or Shapely upgrade takes seconds and catches the class of behaviour change that release notes rarely mention.