All sections

Bounding Box To H3

Normalizing a west/south/east/north bounding box into a valid polygon before polyfilling, and the ordering and wraparound bugs that skip this step invites.

approximatestableh36 min read
Source geometry
bounding_box
Destination geometry
h3_cell_set

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:

  1. 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).
  2. 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.

Illustration

A [W,S,E,N] envelope normalized to a polygon, then intersect-filled at R9.
Rendered from the tested conversion code · A [W,S,E,N] envelope normalized to a polygon, then intersect-filled at R9.
Edge cases affecting this page
  • - Geometries crossing ±180° longitude wrap incorrectly, producing world-spanning artifacts when treated as planar.