Handling Antimeridian and Polar CRS Edge Cases

Work in a projected CRS in which the area is contiguous, and split only the geographic output:

from rasterio.warp import transform_bounds

w, s, e, n = transform_bounds(src.crs, "EPSG:4326", *src.bounds, densify_pts=21)
crosses_antimeridian = w > e            # e.g. w=178.9, e=-178.4
near_pole = max(abs(s), abs(n)) > 84

The failures on this page are rare, which is exactly why they are not handled until a scene from Fiji, Chukotka or Antarctica breaks a pipeline. This page belongs to mastering CRS transformations in rasterio in Core Raster Fundamentals & STAC Mapping.


What Goes Wrong at 180°

A narrow scene, a global bounding box A scene one degree wide straddles the 180 degree meridian, occupying longitudes 179.4 east to 179.6 west. Its correct geographic footprint is two thin strips at either edge of a world map. Taking the minimum and maximum longitude naively produces a box from minus 179.6 to plus 179.4, which covers almost the whole globe and matches every search. One degree of data, 359 degrees of bounding box naive bbox: -179.6 to 179.4 -180 +180 Green: the real footprint, split across the map edge. Red: what min/max produces.

The consequence in a catalog is that such an item matches every spatial search on Earth, and the consequence in a mosaic is that it tries to allocate a raster spanning the globe at full resolution. Both are the same bug: treating longitude as a linear quantity where it wraps.


Environment & Setup

Package Version pin Used for
rasterio >=1.3.0 Bounds transforms and reprojection
pyproj >=3.6 CRS definitions and area-of-use checks
shapely >=2.0 Splitting footprints at the antimeridian
pip install "rasterio>=1.3.0" "pyproj>=3.6" "shapely>=2.0"

Complete Working Example

import rasterio
from rasterio.warp import transform_bounds
from shapely.geometry import MultiPolygon, box


def geographic_footprint(path: str):
    """WGS84 footprint that is correct across the antimeridian."""
    with rasterio.open(path) as src:
        w, s, e, n = transform_bounds(src.crs, "EPSG:4326", *src.bounds, densify_pts=41)

    if w <= e:
        return box(w, s, e, n), [w, s, e, n]

    # Crosses 180°: two boxes, and a STAC-style bbox with west > east
    footprint = MultiPolygon([box(w, s, 180.0, n), box(-180.0, s, e, n)])
    return footprint, [w, s, e, n]


def working_crs_for(lon: float, lat: float) -> str:
    """A projected CRS in which an area around (lon, lat) is contiguous."""
    if lat >= 60:
        return "EPSG:3413"                 # NSIDC Arctic polar stereographic
    if lat <= -60:
        return "EPSG:3031"                 # Antarctic polar stereographic
    zone = int((lon + 180) // 6) % 60 + 1
    return f"EPSG:{(32600 if lat >= 0 else 32700) + zone}"

The STAC specification handles the antimeridian by allowing a bbox whose west value is greater than its east value, which is why the function returns [w, s, e, n] unchanged in that case rather than “fixing” it. Search clients that follow the specification interpret that correctly; code that sorts the values to force w < e recreates the whole-world bug.


Polar Regions

Three grids at 80 degrees north In geographic coordinates, pixels near the pole are extremely narrow in ground distance because meridians converge. UTM zones become thin slivers that meet at the pole. A polar stereographic grid keeps pixels close to square and the region contiguous, which is why polar products are distributed in it. Why polar data uses polar projections geographic pixels 1/6 as wide as tall UTM zones taper to slivers polar stereographic one contiguous, square grid Web Mercator is undefined beyond about 85 degrees and cannot hold polar data at all.

Polar products — sea ice, ice sheet velocity, snow cover — are almost always distributed in polar stereographic grids for exactly these reasons, and the safest approach is to keep them there for all processing. Reprojecting polar data to geographic coordinates for analysis produces pixels whose ground size varies by a factor of five or more across a single scene, which silently biases every area calculation.

Tile servers are the awkward exception, since web maps expect Web Mercator and Web Mercator stops at about 85°. For display, either serve the polar data through a polar-projected web map, or accept that the region above 85° is simply not shown — a limitation of the display, not of the data. The display-side choices are covered in preparing rasters for the web.


Mosaicking Across the Line

Mosaicking scenes on both sides of the antimeridian in WGS84 is where most pipelines finally fail, because the merged extent computed from min and max longitudes is the whole globe. The fix is to do the mosaic in a projected CRS centred on the area — a UTM zone that contains it, or a custom transverse Mercator with its central meridian at 180° — where the scenes are adjacent and the extent is small.

from rasterio.crs import CRS

# A transverse Mercator centred on the antimeridian: contiguous for Fiji, Chukotka, the Aleutians
tm180 = CRS.from_proj4("+proj=tmerc +lon_0=180 +k=0.9996 +x_0=500000 +datum=WGS84 +units=m")

Reproject each scene into that CRS, merge with the usual tools from merging tiles with rasterio merge, and only derive a geographic footprint at the very end, using the split logic above.


Verification

Width, not extent The correct test for an antimeridian footprint is its width in degrees computed with wrap-around, which should be small, not the difference between maximum and minimum longitude, which will be nearly 360 for a crossing scene. Measure the width with wrap-around e - w = -358.0 looks like the whole world (e - w) % 360 = 2.0 the real width Any footprint wider than a few tens of degrees from a single scene is a wrap bug.
footprint, bbox = geographic_footprint("S2B_01KAV_20260714.tif")
w, s, e, n = bbox
width = (e - w) % 360
assert width < 10, f"footprint {width:.1f} degrees wide — wrap not handled"
assert footprint.area < 50, "footprint area implausible for one scene"

A single satellite scene is at most a few degrees across even at high latitude, so any computed width above that is a wrap bug rather than a large scene. That assertion belongs in any ingest step that writes catalog items.


Common Errors

Its bbox spans −180 to 180 because the antimeridian crossing was flattened. Emit the STAC-style bbox with west greater than east.

Mosaic allocation fails with a huge array

The merge extent was computed in WGS84 across the line. Mosaic in a projected CRS centred on the area.

Reprojected polar data has streaks

The warp crossed the pole or the dateline in geographic space. Keep polar data in a polar stereographic CRS.

transform_bounds returns a box missing part of the footprint

Edges were not densified. Pass densify_pts=21 or more so curved edges are sampled.


Frequently Asked Questions

Q: Why does my bounding box span the whole world? Because the raster straddles 180 degrees, so its western edge is near plus 179 and its eastern edge near minus 179. Taking the minimum and maximum gives minus 179 to plus 179 — nearly the entire globe — rather than the narrow strip that actually crosses the meridian.

Q: Which CRS should I use near the poles? A polar stereographic projection — EPSG 3413 or 3995 for the Arctic, 3031 for the Antarctic. Geographic coordinates and Web Mercator both break down there, and UTM zones converge to slivers that are awkward to mosaic.

Q: Does reprojection across the antimeridian work at all? Into a projected CRS centred on the area, yes, and cleanly. Into WGS84 it produces either a raster that wraps the globe or two pieces, depending on the tool, so keep processing in a projected CRS and convert only the final footprint.

Q: How common are these cases in practice? Rarer than most bugs and more damaging when they occur. Pacific island nations, the Russian Far East, Alaska’s Aleutians and both polar regions all trigger them, and a global pipeline will meet all of them eventually — which is why handling them once, centrally, is worth the effort.