A coastline or EEZ breaks at the antimeridian
Problem statement
A dataset that spans 180ยฐ longitude produces a specific and unmistakable set of failures: a country stretched all the way around the world, a centroid in the wrong ocean, a bounding box covering the entire planet, a buffer that disappears, and polygons with a thin sliver connecting two halves.
The cause is that longitude wraps and coordinates do not. A territory with parts at 177ยฐE and 179ยฐW is stored as +177 and โ179, and every function that works with those numbers as plain coordinates โ centroid, bounds, buffer, dissolve, clip, distance โ treats them as 356ยฐ apart rather than 4ยฐ.
Measured on the Natural Earth Fiji polygon: one feature, 44 parts, bounds running from โ180 to +180, 18 parts west of โ170ยฐ and 26 east of 170ยฐ, and a naive centroid longitude of 163.41ยฐ โ roughly 1,500 km from the country.
Quick answer
Detect the crossing, shift to a 0โ360 frame, work, shift back:
import numpy as np, geopandas as gpd
from shapely.ops import transform
def crosses_antimeridian(gdf, margin=10.0):
b = gdf.explode(index_parts=False).bounds
return bool((b.minx < -180 + margin).any() and (b.maxx > 180 - margin).any())
def shift(gdf, to_360=True):
def fn(x, y, z=None):
x = np.asarray(x)
return (np.where(x < 0, x + 360, x) if to_360 else np.where(x > 180, x - 360, x), y)
out = gdf.copy()
out["geometry"] = out.geometry.apply(lambda g: transform(fn, g))
return out
if crosses_antimeridian(zone):
z = shift(zone)
result = shift(gpd.clip(shift(data), z), to_360=False)
In the shifted frame the same Fiji polygon has bounds from 174.59 to 181.78 and a centroid longitude of 178.52 โ the country.
Step-by-step solution
1. Detect the crossing rather than assuming
A geometry whose parts reach both extremes crosses it. So does one whose bounding box spans nearly 360ยฐ for a feature that obviously does not.
b = gdf.total_bounds
suspicious = (b[2] - b[0]) > 350
2. Know the symptoms so you recognise them elsewhere
| symptom | operation |
|---|---|
| centroid in the wrong ocean | centroid |
| bounding box covers the world | total_bounds, sjoin with a bbox |
| polygon stretched across the map | rendering, union_all |
| buffer disappears or explodes | buffer |
| distance is 20,000 km | any planar distance |
3. Shift to 0โ360 for the work
Add 360 to every negative longitude. Everything in the computation must be shifted โ the data, the mask, the study area โ and the result shifted back before writing.
4. Or split at the antimeridian, if the consumer needs โ180 to 180
Splitting each geometry along the 180ยฐ meridian gives valid โ180 to 180 features at the cost of turning one feature into two. GeoJSON's specification recommends this; antimeridian on PyPI implements it properly.
5. Work in a projected CRS where you can
A polar or Pacific-centred projection has no seam where your data is. For a study area entirely in the Pacific, projecting once at the start removes the whole problem.
6. Use ECEF for distances
Converting to Earth-centred Cartesian coordinates and using a k-d tree makes nearest-neighbour and distance queries immune to the seam, because there is no seam in three dimensions.
7. Write in the convention the consumer expects
GeoJSON should be โ180 to 180 and split at the meridian. Internal processing can use whatever is convenient, provided the boundary between the two is explicit.
Code examples
Example 1 โ diagnose a layer
import numpy as np, geopandas as gpd
def antimeridian_report(gdf, margin=10.0):
parts = gdf.explode(index_parts=False)
b = parts.bounds
west = int((b.minx < -180 + margin).sum())
east = int((b.maxx > 180 - margin).sum())
total = gdf.total_bounds
return {
"features": len(gdf), "parts": len(parts),
"parts_near_minus_180": west, "parts_near_plus_180": east,
"crosses": bool(west and east),
"total_bounds": np.round(total, 3).tolist(),
"bbox_width_deg": round(float(total[2] - total[0]), 2),
"naive_centroid_lon": round(float(gdf.geometry.centroid.x.mean()), 2),
}
print(antimeridian_report(fiji))
{'features': 1, 'parts': 44, 'parts_near_minus_180': 18, 'parts_near_plus_180': 26,
'crosses': True, 'total_bounds': [-180.0, -21.711, 180.0, -12.475],
'bbox_width_deg': 360.0, 'naive_centroid_lon': 163.41}
A bounding box 360ยฐ wide on a feature that is 7ยฐ wide is the single clearest signal there is.
Example 2 โ a context manager that shifts and restores
import contextlib, numpy as np, geopandas as gpd
from shapely.ops import transform
def _shift(geom, to_360):
def fn(x, y, z=None):
x = np.asarray(x)
return (np.where(x < 0, x + 360, x) if to_360
else np.where(x > 180, x - 360, x), y)
return transform(fn, geom)
@contextlib.contextmanager
def pacific_frame(*gdfs):
"""Shift every layer to 0โ360 for the duration of the block."""
shifted = []
for g in gdfs:
s = g.copy()
s["geometry"] = s.geometry.apply(lambda q: _shift(q, True))
shifted.append(s)
try:
yield shifted if len(shifted) > 1 else shifted[0]
finally:
pass
with pacific_frame(zone, data) as (z, d):
clipped = gpd.clip(d, z)
clipped["geometry"] = clipped.geometry.apply(lambda q: _shift(q, False))
The important property is that both layers are shifted together. Shifting one and not the other produces an empty clip, which looks like a data problem.
Example 3 โ split at the meridian for output
import numpy as np, geopandas as gpd
from shapely.geometry import box
from shapely.ops import transform
def split_at_antimeridian(gdf):
"""Cut 0โ360 geometries at 180 and return them in โ180 to 180."""
west = box(-180, -90, 180, 90)
east = box(180, -90, 540, 90)
pieces = []
for row in gdf.itertuples():
g = row.geometry
for clip_box, wrap in ((west, False), (east, True)):
part = g.intersection(clip_box)
if part.is_empty:
continue
if wrap:
part = transform(lambda x, y, z=None: (np.asarray(x) - 360, y), part)
pieces.append({**row._asdict(), "geometry": part})
out = gpd.GeoDataFrame(pieces, crs=gdf.crs).drop(columns=["Index"], errors="ignore")
return out
Use this on the way out, not during processing: splitting doubles the feature count and any per-feature attribute then describes half a feature.
Explanation
Why the centroid lands so far away
A centroid is an area-weighted average of coordinates. With 26 parts near +178 and 18 near โ179, the arithmetic pulls the result towards the middle of the numeric range rather than the middle of the geography. Fiji's 163.41ยฐ is not the midpoint of ยฑ180 because the parts are unevenly distributed; it is simply a number with no geographic meaning.
Why a buffer disappears
buffer operates in the coordinate plane. A polygon with parts at +177 and โ179 has a convex hull spanning 356ยฐ, and a buffer of that is a shape covering most of the plane whose intersection with anything useful is either empty or the whole world. The failure mode depends on what happens next, which is why the symptom varies.
Why splitting is the standard for output and not for processing
Splitting yields geometries that every consumer handles correctly, which is why GeoJSON's specification recommends it. It also turns one feature into two, so any attribute that describes the whole โ a population, an area, an identifier โ is now attached to two rows. Doing it as the last step before writing keeps the processing clean and the output correct.
Why ECEF has no seam
Longitude is a coordinate on a cylinder, and a cylinder has a seam. Earth-centred Cartesian coordinates are three numbers on a sphere, and a sphere does not. Any operation expressible as a distance or a nearest-neighbour query โ which is most of them โ is simplest in ECEF and immune to the whole problem.
Edge cases or notes
- Polar data has the same shape of problem at the poles, where longitude is degenerate.
- Raster grids in 0โ360 need a roll, not a slice, to subset across the meridian.
total_boundsof 360ยฐ width is the fastest detector.- Two bounding boxes is the STAC convention for a crossing item.
- Dissolving across the seam produces a sliver through the whole world.
- Tiles handle it by construction; a web map is not evidence the data is fine.
- Check both layers. Shifting one is worse than shifting neither.
- Fiji, Kiribati, Russia, Antarctica and the US all cross it.
Internal links
- How to clip a dataset to an exclusive economic zone โ where this most often bites
- Maritime boundaries explained: EEZ, territorial sea and baselines โ zones that routinely cross
- Longitude conventions explained โ 0โ360 versus โ180 to 180
- H3 hexagons break at the antimeridian โ the same seam in a grid system
- How to compute distance to the coast for many points โ the ECEF approach
- NetCDF maps are shifted or flipped โ the raster version
- How to fix invalid geometries in GeoPandas โ after splitting
- Points plot in the ocean at latitude-longitude zero โ a different coordinate failure
FAQ
Why is my Pacific country stretched across the whole map?
Because its parts are stored at both +177 and โ179 and the renderer joins them across the numeric gap. Shift to a 0โ360 frame or split the geometries at 180ยฐ.
Why is the centroid in the wrong ocean?
A centroid averages coordinates. With parts at both ends of the longitude range, the average is a number with no geographic meaning โ 163.41ยฐ for Fiji.
How do I detect the problem automatically?
Check whether the total bounding box is close to 360ยฐ wide, or whether parts exist near both โ180 and +180.
Should I shift or split?
Shift for processing, split for output. Splitting doubles the feature count, which breaks per-feature attributes.
Does a projected CRS fix it?
For a study area entirely on one side, yes โ a Pacific-centred projection has no seam where your data is.
What about distances?
Convert to Earth-centred Cartesian coordinates and use a k-d tree. There is no seam in three dimensions.