Skip to main content

bboxkit

Bounding boxes, without the guesswork. Parse a bbox from whatever tool handed it to you, convert it to whatever the next tool wants, and do correct geometry on it, including boxes that cross the antimeridian. Pure Python, zero dependencies, fully typed.

pip install bboxkit            # zero dependencies
pip install "bboxkit[pyproj]"  # optional: reproject to any CRS
>>> from bboxkit import BBox, parse
>>> box = parse("29.0,40.9,29.2,41.1", order="wsen")   # Istanbul
>>> box.to_overpass()
'(40.9,29,41.1,29.2)'
>>> box.to_gdal_te()
'-te 29 40.9 29.2 41.1'
>>> round(box.area_km2(), 3)                          # exact on the WGS 84 ellipsoid
373.741

Every tool agrees that a bounding box has four numbers. Almost nothing else is agreed: GeoJSON and Shapely write west,south,east,north, Overpass and Leaflet write south,west,north,east, gdal_translate -projwin wants west,north,east,south, Nominatim returns [south, north, west, east] as strings, and a box around Fiji has west > east. bboxkit is a small, dependency-free value type that knows all of these conventions, so other packages can depend on it instead of re-implementing (and re-debugging) them.

Supported formats

Format Example Parse Format
W,S,E,N (GeoJSON, Shapely, OSM API, STAC) 29,40.9,29.2,41.1 parse(s, order="wsen") to_wsen()
S,W,N,E (lat-first) 40.9,29,41.1,29.2 parse(s, order="swne") to_swne()
bboxfinder.com http://bboxfinder.com/#40.9,29,41.1,29.2 auto to_bboxfinder()
Overpass QL (40.9,29,41.1,29.2), [bbox:40.9,29,41.1,29.2] auto to_overpass()
OSM URL ...?bbox=W,S,E,N, ?minlon=..., #map=14/41.0/29.0 auto (viewport=) to_osm_url()
WKT / EWKT POLYGON ((...)), SRID=3857;MULTIPOINT(...), any geometry auto to_wkt()
ENVELOPE (CQL, Solr) ENVELOPE(29, 29.2, 41.1, 40.9) auto to_wkt_envelope()
PostGIS ST_MakeEnvelope(29, 40.9, 29.2, 41.1, 4326), BOX(29 40.9,29.2 41.1) auto to_postgis()
GeoJSON bbox member, or computed from any geometry / Feature / FeatureCollection auto to_geojson()
Shapely / GeoPandas geom.bounds, gdf.total_bounds, __geo_interface__ parse(obj) to_shapely_bounds()
GDAL / OGR -te 29 40.9 29.2 41.1, -projwin 29 41.1 29.2 40.9, -spat ..., -te_srs auto to_gdal_te(), to_gdal_projwin()
Leaflet / Google [[40.9, 29], [41.1, 29.2]] parse_leaflet() to_leaflet()
Mapbox / MapLibre [[29, 40.9], [29.2, 41.1]] parse_mapbox() to_mapbox()
XYZ tile 14/9375/6142, https://tile.../14/9375/6142.png auto to_tile()
Quadkey (Bing) 1202102332221212 auto to_quadkey()
Geohash sxk9 auto to_geohash()
Plus Code (OLC) 8FVC9G8F+6W auto to_pluscode()
MGRS square 35TPF, 31U DQ 48251 11932 auto to_mgrs()
Dictionaries Nominatim result, ArcGIS extent (wkid), STAC collection, Leaflet _southWest, Google southwest, {minx, ...} parse(d) to_dict()
Place name "Istanbul" nominatim.geocode_bbox() (opt-in, network)

For cell formats (tile, quadkey, geohash, Plus Code, MGRS) parsing gives the cell's box and formatting gives the smallest cell that contains the whole box.

Not included, on purpose: H3 (needs the icosahedral projection, i.e. the h3 library) and the polar UPS part of MGRS.

One-liners

from bboxkit import BBox, parse, union_all

parse("http://bboxfinder.com/#40.9,29,41.1,29.2")         # BBox(29.0, 40.9, 29.2, 41.1)
parse("[bbox:40.9,29,41.1,29.2]")                          # Overpass setting
parse("gdalwarp -t_srs EPSG:32635 -te 500000 4500000 510000 4510000 in.tif out.tif")
parse("https://www.openstreetmap.org/#map=14/41.0082/28.9784", viewport=(1920, 1080))
parse("SRID=3857;POLYGON((0 0, 10 0, 10 10, 0 0))")       # crs='EPSG:3857'
parse({"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[179, 0], [-179, 1]]}})
parse({"boundingbox": ["40.9", "41.1", "29.0", "29.2"]})   # a Nominatim result
parse(shapely_polygon)                                      # uses .bounds
parse("14/9375/6142"); parse("sxk9"); parse("8FVC9G8F+6W"); parse("35TPF")

box = BBox(29.0, 40.9, 29.2, 41.1)
box.to_wkt()               # 'POLYGON ((29 40.9, 29.2 40.9, 29.2 41.1, 29 41.1, 29 40.9))'
box.to_geojson()           # {'type': 'Polygon', 'coordinates': [...], 'bbox': [...]}
box.to_leaflet()           # [[40.9, 29.0], [41.1, 29.2]]
box.to_gdal_projwin()      # '-projwin 29 41.1 29.2 40.9'
box.to_osm_url()           # 'https://www.openstreetmap.org/#map=11/41.00008/29.10000'
box.format("postgis")      # 'ST_MakeEnvelope(29, 40.9, 29.2, 41.1, 4326)'
box.to_geohash(), box.to_tile(), box.to_mgrs()   # ('sxk', Tile(x=2, y=1, z=2), '35TPF')

box.area_km2()             # 373.741450485826  (exact for a lat/lon rectangle on WGS 84)
box.center                 # (29.1, 41.0)
box.width_m, box.height_m  # (16827.0..., 22210.7...)
box.buffer(500)            # grown by 500 m on every side
box.buffer(-500)           # shrunk by 500 m
box.contains((29.1, 41.0)); box.intersects(other); box & other; box | other
box.grid(4, 3)             # 12 cells, row by row from the north-west
box.split(1000)            # ~1 km x 1 km cells
box.tiles(14)              # [Tile(x=9511, y=6136, z=14), ...]
box.tile_count(14)         # 130
box.expand_to_aspect(16 / 9)            # fit a 16:9 web map (Mercator), never shrinks
BBox(29.03, 40.91, 29.17, 41.08).snap(0.1)   # BBox(29.0, 40.9, 29.2, 41.1)
round(box, 2)
box.to_crs("EPSG:3857")    # built in
box.to_utm()               # built in: EPSG:32635 for Istanbul
box.to_crs("EPSG:3035")    # any CRS with the [pyproj] extra
w, s, e, n = box           # unpacks like a tuple

The antimeridian

For longitude/latitude boxes, west > east means the box crosses the 180th meridian, exactly as in RFC 7946:

>>> fiji = BBox(177, -20, -178, -16)
>>> fiji.crosses_antimeridian, fiji.width, fiji.center
(True, 5.0, (179.5, -18.0))
>>> fiji.contains((-179.5, -18))
True
>>> fiji.parts()
(BBox(177.0, -20.0, 180.0, -16.0), BBox(-180.0, -20.0, -178.0, -16.0))
>>> BBox(170, 0, 175, 1) | BBox(-175, 0, -170, 1)
BBox(170.0, 0.0, -170.0, 1.0)
>>> fiji.to_geojson()["type"]            # split into two parts, as RFC 7946 recommends
'MultiPolygon'
>>> fiji.to_leaflet()                    # one contiguous box on a web map
[[-20.0, 177.0], [-16.0, 182.0]]
  • union() / union_all() and bounding boxes computed from GeoJSON or WKT use the smallest longitude range that covers everything (the complement of the largest gap), so the RFC 7946 Fiji example gives [177, -20, -178, -16], not [-178, -20, 177, -16].
  • A line or ring whose consecutive vertices are more than 180 degrees apart is read as crossing the antimeridian (the shorter way round).
  • intersection() returns None, one box, or raises if the overlap is two separate pieces (wide boxes overlapping at both ends); intersection_parts() always returns the list.
  • Formats that cannot express a crossing box (-te, -projwin, ST_MakeEnvelope, Shapely bounds) raise a ValueError that points to parts().
  • In Web Mercator a crossing box keeps contiguous x values (east beyond 20037508.34 m) and converts back to a crossing box.

W,S,E,N or S,W,N,E?

Four bare numbers are ambiguous. bboxkit reads them as west,south,east,north unless only the lat-first reading is valid, and emits an AmbiguousOrderWarning whenever both readings are possible. Say what you mean to make the warning go away:

parse("40.9,29,41.1,29.2", order="swne")
parse("swne:40.9,29,41.1,29.2")
parse(text, fmt="leaflet")

Area, buffers and grids

  • area_km2() uses the closed-form area of a region bounded by two meridians and two parallels on the WGS 84 ellipsoid (via the authalic latitude function). It matches a densely densified pyproj.Geod polygon to ~1e-11. 1 x 1 degree at the equator is 12308.4639 km2, at 60 N 6123.1409 km2; the whole globe is 510065621.724 km2.
  • buffer(m) uses the local radii of curvature; the longitude margin is taken at the most poleward latitude, so every point within m metres of the box is inside the result. Boxes that reach a pole get the full longitude range.
  • split(cell_m) builds a regular lon/lat grid whose cells measure cell_m at the box's central latitude; grid(nx, ny) splits evenly. Both work across the antimeridian.
  • Projected boxes (UTM, any metric CRS) do all of this in CRS units.

Reprojection

to_crs() transforms densified edges (21 points per edge by default) and takes the extent. WGS 84, Web Mercator (EPSG:3857 and its aliases 900913, 3785, 102100) and WGS 84 / UTM (EPSG:326xx, EPSG:327xx, ETRS89 EPSG:258xx) are built in; UTM uses the 6th-order Krueger series (agrees with PROJ to nanometres). Anything else goes to pyproj when it is installed. to_utm() picks the zone of the centre, including the Norway and Svalbard exceptions.

Command line

$ bboxkit convert "29.0,40.9,29.2,41.1" --to overpass
bboxkit: warning: 29,40.9,29.2,41.1 is ambiguous; read as west,south,east,north. ...
(40.9,29,41.1,29.2)
$ bboxkit convert "29.0,40.9,29.2,41.1" --order wsen --to all
$ bboxkit convert "http://bboxfinder.com/#40.9,29,41.1,29.2" --to gdal-projwin
$ bboxkit area "29.0,40.9,29.2,41.1" --order wsen
373.741
$ bboxkit tiles "29.0,40.9,29.2,41.1" --order wsen --zoom 14 --count
130
$ bboxkit tiles "29.0,40.9,29.2,41.1" --order wsen --zoom 14 --format quadkey
$ bboxkit split "29.0,40.9,29.2,41.1" --order wsen --cell 1km --to geojson > grid.geojson
$ bboxkit split "29.0,40.9,29.2,41.1" --order wsen --grid 4x3
$ bboxkit buffer "POINT(29 41)" --distance 2km --to wkt
$ bboxkit reproject "29.0,40.9,29.2,41.1" --order wsen --to-crs utm
$ bboxkit info "177,-20,-178,-16" --order wsen
$ bboxkit geocode "Kadikoy, Istanbul" --user-agent "my-app/1.0 (me@example.org)"
$ bboxkit formats

Boxes that start with a minus sign (-122.5,37.7,-122.3,37.8, -te 1 2 3 4) work as they are; - reads the box from standard input.

Place names (optional, network)

parse() never touches the network. Place lookup is a separate, explicit call:

from bboxkit.nominatim import geocode_bbox

geocode_bbox("Kadikoy, Istanbul", user_agent="my-app/1.0 (me@example.org)", timeout=10)

It follows the Nominatim usage policy: at most one request per second per process, an identifying User-Agent (please set your own), results cached in memory, and a base_url= for your own Nominatim instance when you need more than occasional lookups. Results are (c) OpenStreetMap contributors, ODbL.

Design

  • BBox is immutable, hashable, picklable and slotted. == and hash are exact so boxes can be dict keys; equals(other, tol=1e-9) compares with a tolerance (longitudes modulo 360).
  • Validation on construction: finite numbers, south <= north, latitudes and longitudes in range for geographic CRSs, west <= east for projected ones. BBox.wrap() accepts unwrapped longitudes such as 170 .. 190.
  • CRS strings are normalised (4326, "epsg:4326", URNs, OGC URLs, CRS84 all become "EPSG:4326").
  • Cell lookups (to_geohash(), to_pluscode(), ...) use exact integer arithmetic where the format allows it, so round trips are stable.

Development

pip install -e ".[dev,pyproj]"
pytest --cov=bboxkit
ruff check . && ruff format --check src tests
mypy --strict src

License

MIT, see LICENSE. Copyright (c) 2026 Yusuf Eminoğlu.

Metadata

Release files for bboxkit 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for bboxkit 0.1.0
File Size Uploaded Provenance Actions
bboxkit-0.1.0.tar.gz 68.3 kB With provenance Details for bboxkit-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for bboxkit 0.1.0
File Interpreter ABI Platform Actions
bboxkit-0.1.0-py3-none-any.whl Python 3 none any Details for bboxkit-0.1.0-py3-none-any.whl

Total release size: 119.0 kB

Release files / bboxkit-0.1.0.tar.gz

Download URL bboxkit-0.1.0.tar.gz
Size 68.3 kB
Tags Source
SHA-256 checksum
How to use checksums
f34cc914b28e7c421ea09634a2a5684634e4ea52ee28d8a1e61e9b47a10c5e58
BLAKE2b-256 checksum
How to use checksums
998152a8b1464c00ee22e4b82b4ddf1e1212cced3f842a18f0ff9d4b97f4f07a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.

Transparency log

Release files / bboxkit-0.1.0-py3-none-any.whl

Download URL bboxkit-0.1.0-py3-none-any.whl
Size 50.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3089ca4483fa31555c1459f6697d9653634b4bc55455d92bf2ce2226e19c46b2
BLAKE2b-256 checksum
How to use checksums
4eb429d610529793503ea92ebc2ab2ddf2c72dba14c0b8eb1406ce25c6ee62cc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.

Transparency log

Security for bboxkit 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for bboxkit-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page