Purpose
A bounding box — west, south, east, north — is not itself a
polygon; it is four numbers that imply one only under an assumed
convention (geographic coordinates, west < east, no wraparound). Treating
those four numbers as directly polyfillable without normalizing them first
is the single most common source of silently wrong bounding-box conversions,
particularly for any box that crosses the antimeridian.
Source geometry and destination geometry
Source geometry is bounding_box: a (west, south, east, north) tuple,
typically produced by a map viewport, a vendor's coarse "service area," or
a quick-and-dirty spatial filter. Destination geometry is an h3_cell_set
after the box is converted to a proper polygon and polyfilled by the
arbitrary polygon machinery.
Exactness class
Approximate, inheriting the exactness class of whichever polygon containment mode is applied after normalization — a bounding box carries no information about the "real" shape it is meant to approximate, so it should be treated as a crude proxy geometry, not a precise target, in any use case where the box is standing in for something else (a service area, a rough market boundary).
Containment rule and boundary behavior
Conversion is two steps, and the first step is where correctness is actually decided:
- Normalize: turn
(west, south, east, north)into a closed, correctly wound polygon in EPSG:4326. This requires resolving three ambiguities before a polygon can be constructed at all — coordinate validity, west/east ordering, and antimeridian crossing (below). - Polyfill: apply any of the four standard containment modes
(
center,full,intersect,threshold) to the normalized polygon, exactly as on the arbitrary polygon page.
Normalization must check, in order: that south < north (a box with
south > north is invalid, not merely reversed, since latitude does not
wrap); whether west > east in a way that indicates antimeridian crossing
rather than an invalid box (see edge cases); and that all four values fall
within valid ranges (latitude in [-90, 90], longitude in [-180, 180])
before any polygon is built. A rotated bounding box — one expressed in
screen-space (pixel or tile) coordinates rather than geographic ones — is
not a case this conversion handles at all; it must first be reprojected to
geographic west/south/east/north, since a box that is axis-aligned on
screen is not axis-aligned on the geographic grid except at very coarse
zoom levels.
Resolution behavior
Ordinary polyfill resolution behavior applies once the box is a normalized polygon: the chosen containment mode's coverage error shrinks with finer resolution exactly as for any rectangular polygon. There is no box-specific resolution consideration beyond noting that a bounding box is usually a much coarser proxy for the caller's actual target than the resolution suggests — polyfilling a viewport bounding box at res 10 does not make the box a more accurate representation of anything, it just produces a finer-grained approximation of a shape that was already an approximation.
Units and CRS
Input coordinates must be EPSG:4326 decimal degrees. A box supplied in Web Mercator (EPSG:3857) tile bounds — common from map-viewport APIs — must be reprojected to geographic coordinates before normalization; failing to do so produces a box with plausible-looking but wrong degree values, especially near the poles where Web Mercator's distortion is most severe.
Algorithm
import { normalizePolygon } from "@/lib/geometry/normalize";
import { polygonToH3 } from "@/lib/h3/polyfill";
function bboxToH3(west, south, east, north, resolution, mode) {
if (south > north) {
throw new Error("invalid box: south must be <= north");
}
// west > east signals antimeridian crossing, not an invalid box;
// normalizePolygon must split the resulting polygon at +/-180.
const rawPolygon = boxToPolygon(west, south, east, north);
const polygon = normalizePolygon(rawPolygon); // splits at antimeridian if needed
return polygonToH3(polygon, { resolution, mode });
}
The same conversion with the Python bindings (h3-py v4):
import h3
def bbox_to_h3(west, south, east, north, res):
if south > north:
raise ValueError("invalid box: south must be <= north")
if west <= east:
# Ordinary box: one LatLngPoly, counter-clockwise ring
ring = [(south, west), (south, east), (north, east), (north, west)]
return set(h3.polygon_to_cells(h3.LatLngPoly(ring), res))
# west > east signals antimeridian crossing, not an invalid box: split
# into two boxes at +/-180 and union the polyfilled results.
west_ring = [(south, west), (south, 180), (north, 180), (north, west)]
east_ring = [(south, -180), (south, east), (north, east), (north, -180)]
west_cells = h3.polygon_to_cells(h3.LatLngPoly(west_ring), res)
east_cells = h3.polygon_to_cells(h3.LatLngPoly(east_ring), res)
return set(west_cells) | set(east_cells)
cells = bbox_to_h3(170, 10, -170, 20, res=6) # antimeridian-crossing box
h3.polygon_to_cells is center containment only, matching the center
mode above; full/intersect/threshold need the same per-cell
shapely classification described on the
arbitrary polygon page, applied to
whichever normalized (and, if needed, antimeridian-split) polygon results
from bbox_to_h3's normalization step. The tested reference
implementation for this conversion is the TypeScript in lib/.
Parameters
The four box coordinates, H3 resolution, containment mode, and an explicit
assumeAntimeridianCrossing flag or equivalent so that west > east is
handled deliberately rather than inferred silently from the sign of the
difference alone.
Outputs
An h3_cell_set at the stated resolution and mode; for a box that crosses
the antimeridian, the output correctly includes cells on both the
easternmost and westernmost sides of the +/-180 meridian rather than
either an empty result or a result covering the wrong (much larger)
hemisphere.
Quality metrics
coverage_ratio, overreach_ratio, and jaccard computed against the
normalized polygon, not against the raw four-number box — since the box has
no area itself, these metrics are only meaningful once it has been turned
into a polygon. A useful box-specific diagnostic is polygon area versus the
naive (east - west) * (north - south) product: a large discrepancy is a
signal that antimeridian handling or high-latitude distortion is present
and should be checked.
Edge cases
Antimeridian crossing is the dominant failure mode for this conversion. A
box describing, for example, the Pacific region spanning west = 170 to
east = -170 has west > east in raw numeric terms, but this is a valid
20-degree-wide box that crosses +/-180, not an inverted or invalid one; a
naive implementation that assumes west < east will either construct a
polygon spanning the wrong 340-degree remainder of the globe or throw on
south/north-style validation logic misapplied to longitude. Correct
handling splits the box into two polygons — one from west to 180, one
from -180 to east — polyfills each independently, and unions the
resulting cell sets; see antimeridian handling
for the general treatment this conversion depends on. Distinguishing a
genuinely invalid box (west > east due to a data error, with no crossing
intended) from a legitimate antimeridian-crossing box requires either an
explicit caller flag or a heuristic threshold (e.g. only treat west > east
as crossing if the implied "short way" span is under some maximum width),
and that heuristic should be documented wherever it is applied rather than
left implicit, since it is a business-logic assumption, not a geometric
fact.
Assumptions and limitations
This conversion assumes the four input numbers are genuinely geographic west/south/east/north bounds in EPSG:4326; screen-space, tile-space, or rotated bounding regions must be converted to that form first, and no amount of careful antimeridian handling here corrects for a box that was never geographic to begin with. It also assumes a normalized polygon is produced and validated before polyfilling — skipping normalization "because the box looks fine" is exactly the failure mode that antimeridian-crossing boxes exploit, since they look fine as four numbers and only fail once naively converted to a polygon.
