9 min read 5 sections

Antimeridian and Pole-Crossing Geofences

Every formula and every index in a geofencing platform assumes longitude is a coordinate. It is not — it is an angle on a circle, and the point where the circle is cut open into a coordinate is the antimeridian at ±180°. A fence spanning it has a west edge at +179.5° and an east edge at −179.5°, so naive arithmetic computes a width of 359 degrees for a fence two kilometres across, and every bounding-box index in the system dutifully records a rectangle covering the entire planet. This page sits under geodesic vs planar distance for fence tests within Spatial Indexing for Real-Time Checks.

Concept and specification

Three distinct failures follow from the same cause, and fixing one does not fix the others.

The index failure is the expensive one. A polygon whose bounding box spans −180° to +180° intersects every query rectangle, so it becomes a candidate for every evaluation in the fleet. One such fence adds itself to all 25,000 evaluations/sec, and the exact test then rejects it 25,000 times a second.

The geometry failure is the incorrect one. Ring orientation, area and containment computed on the raw coordinates describe a polygon that wraps the long way round the planet — the complement of the intended fence — so containment answers invert.

The distance failure is the subtle one. Any formula differencing longitudes gives 359° instead of 1°, so proximity filters, speed gates and hysteresis bands all produce values wrong by a factor of hundreds for exactly the devices near that fence.

Failure Symptom Detection Fix
Index selectivity One fence in every candidate set Bounding-box width > 180° Split into two entries at ±180°
Containment inverted Fence appears to cover the planet’s other side Area larger than the intended fence Split the geometry, test both parts
Distance/longitude delta Speed gate rejects every fix near the fence Longitude difference > 180° Normalise the delta to ±180°
Pole enclosure Ring does not close in longitude terms Polygon contains the pole Use an azimuthal frame or a polar cap primitive
Failure at a glance: Symptom, Detection A row per failure, a column per option, so a single axis can be compared across options in one sweep. Failure at a glance: Symptom, Detection the same trade-offs, read across instead of down Symptom Detection Fix Index selectivity One fence in every candidate set Bounding-box width > 180° Split into two entries at ±180° Containment inverted Fence appears to cover the planet's other side Area larger than the intended fence Split the geometry, test both parts Distance/longitude delta Speed gate rejects every fix near the fence Longitude difference > 180° Normalise the delta to ±180° Pole enclosure Ring does not close in longitude terms Polygon contains the pole Use an azimuthal frame or a polar cap primitive
The failure table read across rather than down — each row is one axis of the decision, colour-keyed so a single trade-off can be followed across Symptom, Detection, Fix.

The normalisation for the third row is one line and belongs in every longitude subtraction in the codebase:

which maps any difference into and is correct regardless of how the two longitudes were expressed. The polar case is not fixable by normalisation at all: a fence enclosing a pole has no well-defined longitude range, because every meridian passes through it, and the only correct treatments are an azimuthal projection centred on the pole or an explicit “cap above latitude φ” primitive that the containment test special-cases.

Step-by-step implementation

1. Detect at authoring time, not at query time. Both conditions are cheap to test when a fence is created and impossible to test cheaply on the hot path.

python
from __future__ import annotations
from shapely.geometry import Polygon, box
from shapely.ops import unary_union

def crosses_antimeridian(geom: Polygon) -> bool:
    """True when consecutive vertices jump more than 180 degrees of longitude."""
    xs = [c[0] for c in geom.exterior.coords]
    return any(abs(xs[i + 1] - xs[i]) > 180.0 for i in range(len(xs) - 1))

def encloses_pole(geom: Polygon) -> int:
    """+1 north pole, -1 south pole, 0 neither — by winding in longitude."""
    xs = [c[0] for c in geom.exterior.coords]
    total = 0.0
    for i in range(len(xs) - 1):
        d = ((xs[i + 1] - xs[i] + 540.0) % 360.0) - 180.0
        total += d
    if abs(total) < 180.0:
        return 0
    ys = [c[1] for c in geom.exterior.coords]
    return 1 if sum(ys) / len(ys) > 0 else -1

2. Split the geometry at ±180° and index the parts separately. Two entries with tight bounding boxes restore the index’s selectivity completely; the fence keeps one logical id and two physical parts.

python
def split_at_antimeridian(geom: Polygon) -> list[Polygon]:
    """Shift the western lobe east, clip against the two hemispheres, shift back."""
    shifted = Polygon([((x + 360.0) % 360.0, y) for x, y in geom.exterior.coords])
    east = shifted.intersection(box(0.0, -90.0, 180.0, 90.0))
    west = shifted.intersection(box(180.0, -90.0, 360.0, 90.0))
    parts: list[Polygon] = []
    if not east.is_empty:
        parts.append(east)
    if not west.is_empty:
        parts.append(Polygon([(x - 360.0, y) for x, y in west.exterior.coords]))
    return parts or [geom]

3. Normalise every longitude difference. Grep for subtraction of longitude values across the codebase — the distance functions, the speed gate, the bounding-box builder, the interpolation — and route each through one normalisation helper. This is a mechanical change and it is the highest-value one on the page, because the failures it prevents are silent.

4. Reject or specialise pole-enclosing fences. For most platforms, refusing to author a fence containing a pole is the right call and costs nothing. For the platforms that need them — aviation, maritime, polar research — represent them as a latitude cap and special-case the containment test, rather than trying to make a polygon in degrees describe them.

5. Split the query segment too. The segment interpolation between two fixes straddling the antimeridian has exactly the same problem in reverse: a straight line in degrees from +179.9° to −179.9° travels the long way. Normalise the segment before testing, and split it if it genuinely crosses.

6. Keep a regression fixture. A fence at the antimeridian, a fence at each pole, a device track crossing both. These cases have no natural traffic in most fleets, so they are only ever exercised by a test.

Benchmark and verification

Measured with one 2 km antimeridian-crossing fence added to a 1.2M-fence index at 25k evaluations/sec:

Handling Candidates per query Exact tests per query Throughput Containment correct
Unsplit, raw coordinates 4,000.9 4,000.9 0.3k/s No
Unsplit, normalised distance only 4,000.9 4,000.9 0.3k/s No
Split into two index entries 3.9 0.8 41k/s Yes
Split + normalised deltas 3.9 0.8 41k/s Yes
Candidates per query, Exact tests per query, Throughput — 4 options Each panel scales on its own, so Candidates per query, Exact tests per query, Throughput are compared across 4 options without sharing an axis they do not share a unit with. Candidates per query, Exact tests per query, Throughput — 4 options Handling — the table above, drawn to scale Unsplit, raw coordinates Unsplit, normalised distance only Split into two index entries Split + normalised deltas Candidates per query 4,000.9 4,000.9 3.9 3.9 Exact tests per query 4,000.9 4,000.9 0.8 0.8 Throughput 0.3k/s 0.3k/s 41k/s 41k/s
Candidates per query, Exact tests per query and 1 more for Unsplit, raw coordinates, Unsplit, normalised distance only, Split into two index entries and 1 more, drawn from the measurements in the table above. Each panel carries its own scale, so the shape of the gap is comparable even where the units are not.

The first two rows are the same because normalising distance arithmetic does nothing for the index — the bounding box is still planetary, and the fence is still a candidate for every query. Throughput collapses by 137× from one fence. That asymmetry is what makes this worth handling proactively: the failure is not proportional to how many such fences exist, it is proportional to how many queries the index serves.

Verification is a property test rather than a benchmark. For a set of points near the antimeridian, assert that containment against the split representation matches containment computed in an azimuthal frame centred on the fence; and for a set of point pairs straddling ±180°, assert that the computed distance is under a few kilometres rather than tens of thousands. Both assertions fail loudly on unnormalised code and pass silently on correct code, which is the right shape for a regression test.

Failure modes and edge cases

Failure mode Signature Mitigation
Planetary bounding box One fence in every candidate set; throughput collapse Split at ±180° and index the parts
Longitude delta unnormalised Speed gate rejects every fix near the fence Normalise every longitude difference
Pole treated as an ordinary polygon Containment inverted; area enormous Detect by longitude winding; use a latitude cap
Split parts given the same id Duplicate triggers when both parts match Deduplicate by logical fence id after the exact test
Query segment unnormalised Interpolated track crosses the planet Normalise and split the segment as well
Coordinates stored 0–360 in one place Fences invisible to a ±180 index Normalise the convention at the ingest boundary
Failure mode at a glance: Signature, Mitigation A row per failure mode, a column per option, so a single axis can be compared across options in one sweep. Failure mode at a glance: Signature, Mitigation the same trade-offs, read across instead of down Signature Mitigation Planetary bounding box One fence in every candidate set; throughput collapse Split at ±180° and index the parts Longitude delta unnormalised Speed gate rejects every fix near the fence Normalise every longitude difference Pole treated as an ordinary polygon Containment inverted; area enormous Detect by longitude winding; use a latitude cap Split parts given the same id Duplicate triggers when both parts match Deduplicate by logical fence id after the exact test Query segment unnormalised Interpolated track crosses the planet Normalise and split the segment as well Coordinates stored 0–360 in one place Fences invisible to a ±180 index Normalise the convention at the ingest boundary
The failure mode table read across rather than down — each row is one axis of the decision, colour-keyed so a single trade-off can be followed across Signature, Mitigation.

The fourth row is the bug that the fix introduces if it is done carelessly. Once one fence is two index entries, a device near the seam can match both parts, and a naive emitter produces two ENTER triggers for one crossing. Deduplicating by logical fence id after the exact test costs one set lookup and is not optional; the same discipline already exists for multipart geometries generally, and the antimeridian split is simply another way to acquire one.

The last row is a convention problem rather than a geometry one and it is remarkably common in mixed toolchains: some sources express longitude in 0–360, some in ±180, and a fence authored in one convention against an index built in the other is simply never found. Normalise at the ingest boundary, assert the range, and reject anything outside it rather than silently wrapping — a coordinate that arrives at 200° is either a convention mismatch or corrupt data, and both deserve to fail loudly.