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.

A two-degree feature with a 358-degree bounding box A longitude axis runs from minus 180 to plus 180 degrees. A small polygon is drawn straddling the plus and minus 180 line, spanning only two degrees of real extent. Below it, the box produced by taking the minimum and maximum of its coordinates is drawn spanning from minus 179 all the way to plus 179 degrees, nearly the entire axis. Three downstream consequences are listed: a spatial index returns almost every feature as a candidate so the query degenerates to a full scan, a tile query requests tiles across the whole world, and a naive centroid computed from the coordinates lands near zero degrees longitude in the Atlantic rather than near the date line. −180° +180° Real extent of the feature: 2°, straddling the date line Box from min/max of the coordinates: 358° −179° … +179° — nearly the whole planet Everything downstream of the box inherits the error: · the spatial index returns almost every feature as a candidate — the query degenerates to a full scan · a tile request fans out across the world instead of two tiles · a naive centroid lands near 0° longitude, in the Atlantic, on the opposite side of the Earth

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
Three deliberate strategies and one default Four small panels. The split panel shows the date line as a dashed vertical with a separate polygon part on each side, each carrying its own small bounding box. The shift panel shows a single contiguous polygon crossing the line, with an annotation that longitude values now exceed 180 degrees and leave the standard range. The reproject panel shows one contiguous polygon in a regional coordinate system where the date line is not a boundary at all. The do-nothing panel shows the feature rendered as a wide horizontal stripe spanning the whole frame, which is what a naive bounding box produces. Split two parts, two small boxes Shift one part, longitude > 180° Reproject no date line in this CRS Do nothing a stripe across the world The fourth panel is the default, and it is the only one that is unambiguously wrong. The other three are trade-offs, which is why the test must assert the strategy the pipeline declares rather than a universal “correct” shape. Write the assertion against the declared strategy, and name the strategy in the test name. A test called test_antimeridian_is_split says what the pipeline promises; one called test_antimeridian says nothing at all.

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"
Latitude bands where Web Mercator stops working A latitude axis from zero to ninety degrees, divided into three bands. The band from zero to eighty-five degrees is marked as projecting normally. The band from eighty-five degrees to just below ninety is marked as conventionally truncated, with two possible tool behaviours listed: silently clamping the latitude, which moves the feature, or returning an infinite coordinate that becomes not-a-number downstream. The point at ninety degrees is marked as mathematically undefined. Beneath the axis, a polar stereographic coordinate system is shown covering the same range with a single note that the pole is an ordinary point there and none of the three failures arises. Web Mercator, by latitude 85° 90° projects normally truncated clamped or infinite undefined In the truncated band a tool does one of two things, and neither raises: · clamps the latitude to 85° — the feature silently moves, and every later assertion is about the moved copy · returns an infinite coordinate — which becomes NaN in the next arithmetic and poisons the whole frame Polar stereographic: the pole is an ordinary point — no truncation, no clamping, no undefined value. Reproject rather than tolerate.

Failure modes and edge cases

  1. Bounding-box operations before splitting. Any bbox, centroid or buffer on an unsplit anti-meridian geometry is wrong; split at the dateline first.
  2. Web Mercator at the pole. Projecting ±90° to EPSG:3857 yields infinite y; guard with a latitude check and use a polar CRS.
  3. Fixtures stored reprojected. Saving the anti-meridian fixture already split hides the raw-crossing case; keep the unsplit WGS84 form as the fixture.
  4. ±179.99° false positives. A near-dateline feature that does not actually cross should not be split; test both crossing and adjacent cases.
  5. 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.