All sections

Antimeridian Handling

Geometries and circles that cross the ±180° meridian wrap incorrectly under planar longitude math and must be split or unwrapped before any H3 or area operation.

exactstableh35 min read
Source geometry
geofence, point_radius, trade_area, bounding_box
Destination geometry
h3_cell_set

Purpose

Longitude is a coordinate on a circle, not a line, and every operation here that treats it as a line — ring winding, bounding-box construction, buffer generation, planar intersection — breaks silently for any geometry crossing the ±180° meridian. This page states the exact detection test and exact mitigation so "handle the antimeridian" is never an unstated assumption.

The problem

A polygon with vertices at longitude 179.5° and −179.8° is 0.7° wide across the meridian, but a planar algorithm reading those numbers on a continuous line computes a span of 359.3° — effectively the entire globe minus a sliver. The same failure hits point-radius circles (a circle centered at 179.9° with a 50 km radius has an arc crossing 180°) and bounding boxes (a box whose "west" edge is numerically larger than its "east" edge, e.g. west = 170°, east = −170°, is a valid wrapping box, not an invalid one).

Source geometry and destination geometry

Source geometry is any polygon, point-radius circle, or bounding box normalized to EPSG:4326 decimal degrees. Destination geometry is the same geometry type, corrected, or an h3_cell_set if the corrected geometry is then polyfilled. This page is a precondition for geometry-normalization, not an alternative to it — correction must run before self-intersection repair and before polyfilling, since polyfilling an unsplit, meridian-crossing ring returns either zero cells or every cell on the globe depending on which winding-order edge case the library hits.

Exactness class

Exact. Correction is a coordinate-space transform, not an approximation: the corrected geometry represents the identical physical region as the source, with no coverage or area error introduced.

Detection

Longitude span check
For a ring, compute max(lng) − min(lng) using raw signed longitudes. A span greater than 180 degrees indicates the ring crosses the antimeridian (a correctly-behaved ring never legitimately spans more than 180 degrees in a single unwrapped pass).
Sign change across consecutive vertices
Walk the ring in order; if consecutive vertices flip sign (e.g. 179.6 then -179.9) AND the absolute difference exceeds 180 degrees, that edge crosses the meridian.
Bounding box west greater than east
A bounding box where west > east in raw signed degrees (e.g. west=170, east=-170) is a valid antimeridian-wrapping box, not a malformed one — reject any validator that treats west > east as an error.
Circle center near ±180°
For point-radius, flag any circle whose center longitude is within radius/111320 degrees (approximate meters-per-degree at the equator) of ±180°, since its geodesic buffer may cross regardless of the exact center value.

Mitigation

Split at the antimeridian
Cut the ring into two (or more) closed rings at longitude = 180 / -180, producing a valid multipolygon where each part has an unwrapped, non-crossing longitude range. This is the standard GeoJSON-correct representation.
Great-circle densification before splitting
Insert intermediate vertices along the true geodesic between the two vertices that straddle the meridian, so the split point is computed from the actual edge path rather than a straight planar interpolation, which is itself distorted near the pole-adjacent regions of a wide crossing.
Longitude unwrapping for local operations
For operations confined to a small window around the crossing (e.g. computing area or a local buffer), shift longitudes by +360 degrees wherever they are negative, so the whole geometry lies in a single continuous range (e.g. 179 to 181 instead of 179 to -179). Unwrap only for the scope of the local computation; never persist unwrapped coordinates as the canonical representation.
h3-js isGeoJson handling
h3-js's `polygonToCells` accepts a `GeoJsonPolygon` flag that changes ring-crossing behavior at the meridian; confirm which convention (raw signed degrees vs. pre-split multipolygon) the call site expects before passing a crossing ring directly, since passing an unsplit ring to a function expecting pre-split input silently returns the wrong hemisphere's cells.

Resolution behavior

Correction is resolution-independent — a coordinate transform applied once, before polyfilling, whose correctness does not change with the H3 resolution chosen downstream. What does change with resolution is the number of cells near the meridian whose own boundary crosses ±180°: cellToBoundary output for those cells needs the same split/unwrap logic before being rendered or intersected, since H3 cell boundaries are not automatically corrected for meridian crossing by all consumers.

Units and CRS

EPSG:4326 decimal degrees throughout. Splitting and densification run in geodesic (great-circle) space, not a planar-projected CRS — projecting first would require a projection centered away from the crossing, which reintroduces the same problem at a different meridian.

Algorithm

import { normalizePolygon } from "@/lib/geometry/normalize";
import { polygonToH3 } from "@/lib/h3/polyfill";

// normalizePolygon detects span > 180deg and splits into a multipolygon
// with each part's longitudes in a continuous, non-wrapping range.
const normalized = normalizePolygon(rawPolygon);

// Polyfill each part independently; union the resulting cell sets.
const cells = normalized.parts.flatMap((part) =>
  polygonToH3(part, { resolution: 8, mode: "intersect" }),
);

Parameters

None beyond the input geometry — a detection-and-correction pass, not a tunable conversion. The only implicit parameter is the densification sample count used to insert intermediate vertices before computing the split point.

Outputs

A GeoJSON multipolygon (or corrected bounding box / circle) with no ring spanning more than 180° of raw longitude, ready for downstream normalization and polyfilling.

Quality metrics

coverage_ratio and jaccard computed against the source, both of which should equal 1.0 for a correct split (the correction is exact, so any deviation indicates a bug in the split/densification, not an accepted approximation).

Edge cases

Antimeridian is itself the edge case this page documents; it compounds with self-intersections when a source polygon both crosses the meridian and has invalid winding — splitting must run first, since repairing winding on an unsplit ring is undefined.

Assumptions and limitations

This page assumes the source geometry is otherwise valid GeoJSON — closed rings, no self-intersections away from the meridian. It does not address pole-adjacent densification distortion at very high latitudes near the crossing, best handled by increasing the sample count, not a different algorithm.

Illustration — naive fill vs antimeridian split

A box spanning 160°E to 160°W: the naive planar fill (pink) wraps the wrong way around the globe; the correct result splits into two strips hugging ±180 (green).
Rendered from the tested conversion code · A box spanning 160°E to 160°W: the naive planar fill (pink) wraps the wrong way around the globe; the correct result splits into two strips hugging ±180 (green).
Edge cases affecting this page
  • - Geometries crossing ±180° longitude wrap incorrectly, producing world-spanning artifacts when treated as planar.