bboxkit 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bboxkit-0.1.0/CHANGELOG.md +35 -0
- bboxkit-0.1.0/CITATION.cff +20 -0
- bboxkit-0.1.0/LICENSE +21 -0
- bboxkit-0.1.0/MANIFEST.in +8 -0
- bboxkit-0.1.0/PKG-INFO +270 -0
- bboxkit-0.1.0/README.md +233 -0
- bboxkit-0.1.0/pyproject.toml +121 -0
- bboxkit-0.1.0/setup.cfg +4 -0
- bboxkit-0.1.0/src/bboxkit/__init__.py +31 -0
- bboxkit-0.1.0/src/bboxkit/__main__.py +6 -0
- bboxkit-0.1.0/src/bboxkit/_geo.py +222 -0
- bboxkit-0.1.0/src/bboxkit/_version.py +1 -0
- bboxkit-0.1.0/src/bboxkit/bbox.py +1029 -0
- bboxkit-0.1.0/src/bboxkit/cli.py +311 -0
- bboxkit-0.1.0/src/bboxkit/crs.py +380 -0
- bboxkit-0.1.0/src/bboxkit/formats.py +86 -0
- bboxkit-0.1.0/src/bboxkit/geohash.py +83 -0
- bboxkit-0.1.0/src/bboxkit/mgrs.py +136 -0
- bboxkit-0.1.0/src/bboxkit/nominatim.py +113 -0
- bboxkit-0.1.0/src/bboxkit/parsing.py +722 -0
- bboxkit-0.1.0/src/bboxkit/pluscode.py +125 -0
- bboxkit-0.1.0/src/bboxkit/py.typed +0 -0
- bboxkit-0.1.0/src/bboxkit/tiles.py +148 -0
- bboxkit-0.1.0/src/bboxkit.egg-info/PKG-INFO +270 -0
- bboxkit-0.1.0/src/bboxkit.egg-info/SOURCES.txt +35 -0
- bboxkit-0.1.0/src/bboxkit.egg-info/dependency_links.txt +1 -0
- bboxkit-0.1.0/src/bboxkit.egg-info/entry_points.txt +2 -0
- bboxkit-0.1.0/src/bboxkit.egg-info/requires.txt +9 -0
- bboxkit-0.1.0/src/bboxkit.egg-info/top_level.txt +1 -0
- bboxkit-0.1.0/tests/conftest.py +55 -0
- bboxkit-0.1.0/tests/test_bbox.py +261 -0
- bboxkit-0.1.0/tests/test_cells.py +136 -0
- bboxkit-0.1.0/tests/test_cli.py +166 -0
- bboxkit-0.1.0/tests/test_formats.py +145 -0
- bboxkit-0.1.0/tests/test_nominatim.py +80 -0
- bboxkit-0.1.0/tests/test_ops.py +381 -0
- bboxkit-0.1.0/tests/test_parsing.py +373 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project
|
|
5
|
+
uses [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-11-08
|
|
10
|
+
### Added
|
|
11
|
+
- `BBox(west, south, east, north, crs="EPSG:4326")`: immutable, hashable, picklable value
|
|
12
|
+
type with validation, canonical antimeridian edges, `west > east` for boxes crossing the
|
|
13
|
+
antimeridian, exact `==`/`hash` and tolerance-based `equals()`.
|
|
14
|
+
- `parse()` for W,S,E,N / S,W,N,E text (explicit `order=` or a heuristic with
|
|
15
|
+
`AmbiguousOrderWarning`), bboxfinder.com links, Overpass `(s,w,n,e)` and `[bbox:...]`,
|
|
16
|
+
OSM URLs (`bbox=`, `minlon=...`, `map=zoom/lat/lon` with a viewport size), WKT/EWKT,
|
|
17
|
+
`ENVELOPE`, PostGIS `BOX`/`ST_MakeEnvelope`, GeoJSON (bbox member or computed with the
|
|
18
|
+
RFC 7946 antimeridian rules), GDAL `-te`/`-projwin`/`-spat`, Leaflet and Mapbox bounds,
|
|
19
|
+
XYZ tiles and tile URLs, quadkeys, geohashes, Plus Codes, MGRS squares, Shapely/GeoPandas
|
|
20
|
+
objects, `__geo_interface__`, Nominatim/ArcGIS/STAC/Google/Leaflet dictionaries.
|
|
21
|
+
- Formatters for all of the above (`to_overpass()`, `to_gdal_te()`, `to_wkt()`,
|
|
22
|
+
`to_geojson()`, ...) and a `format()` registry.
|
|
23
|
+
- Antimeridian-aware `contains`, `intersects`, `intersection`, `intersection_parts`,
|
|
24
|
+
`union`/`union_all` (smallest covering longitude range).
|
|
25
|
+
- `buffer()` in metres, exact ellipsoidal `area_km2()`, `center`, `width_m`/`height_m`,
|
|
26
|
+
`grid()`, `split()` by cell size in metres, XYZ `tiles()`, `expand_to_aspect()`, `snap()`
|
|
27
|
+
and `round()`.
|
|
28
|
+
- Built-in reprojection to and from Web Mercator and WGS 84 / UTM (6th-order Krueger series)
|
|
29
|
+
with densified edges; any other CRS through the optional `pyproj` extra.
|
|
30
|
+
- Opt-in Nominatim place lookup (`bboxkit.nominatim`) that follows the OSMF usage policy.
|
|
31
|
+
- `bboxkit` command line: `convert`, `info`, `area`, `tiles`, `split`, `buffer`,
|
|
32
|
+
`reproject`, `geocode` and `formats`.
|
|
33
|
+
|
|
34
|
+
[Unreleased]: https://github.com/YusufEminoglu/bboxkit_sdk/compare/v0.1.0...HEAD
|
|
35
|
+
[0.1.0]: https://github.com/YusufEminoglu/bboxkit_sdk/releases/tag/v0.1.0
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use this software, please cite it as below."
|
|
3
|
+
authors:
|
|
4
|
+
- family-names: "Eminoğlu"
|
|
5
|
+
given-names: "Yusuf"
|
|
6
|
+
email: "yusufeminoglu@gmail.com"
|
|
7
|
+
orcid: "https://orcid.org/0009-0005-6000-2934"
|
|
8
|
+
title: "bboxkit"
|
|
9
|
+
version: "0.1.0"
|
|
10
|
+
date-released: "2026-11-08"
|
|
11
|
+
license: "MIT"
|
|
12
|
+
repository-code: "https://github.com/YusufEminoglu/bboxkit_sdk"
|
|
13
|
+
url: "https://pypi.org/project/bboxkit/"
|
|
14
|
+
abstract: "Zero-dependency Python toolkit for bounding boxes: parse and format every common bbox convention, antimeridian-aware geometry, exact ellipsoidal area, tiles and reprojection."
|
|
15
|
+
keywords:
|
|
16
|
+
- bounding box
|
|
17
|
+
- GIS
|
|
18
|
+
- antimeridian
|
|
19
|
+
- GeoJSON
|
|
20
|
+
- web mercator
|
bboxkit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yusuf Eminoğlu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
bboxkit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bboxkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Zero-dependency bounding boxes: parse and format every bbox convention (WKT, GeoJSON, Overpass, GDAL, tiles, geohash, Plus Codes, MGRS), antimeridian-aware geometry, ellipsoidal area and reprojection.
|
|
5
|
+
Author-email: Yusuf Eminoğlu <yusufeminoglu@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/YusufEminoglu/bboxkit_sdk
|
|
8
|
+
Project-URL: Repository, https://github.com/YusufEminoglu/bboxkit_sdk
|
|
9
|
+
Project-URL: Issues, https://github.com/YusufEminoglu/bboxkit_sdk/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/YusufEminoglu/bboxkit_sdk/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: bbox,bounding-box,gis,geojson,wkt,overpass,openstreetmap,gdal,xyz-tiles,quadkey,geohash,plus-codes,mgrs,utm,web-mercator,antimeridian
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Intended Audience :: Science/Research
|
|
24
|
+
Classifier: Intended Audience :: Developers
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Provides-Extra: pyproj
|
|
30
|
+
Requires-Dist: pyproj>=3.4; extra == "pyproj"
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
34
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
35
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# bboxkit
|
|
39
|
+
|
|
40
|
+
**Bounding boxes, without the guesswork.** Parse a bbox from whatever tool handed it to you,
|
|
41
|
+
convert it to whatever the next tool wants, and do correct geometry on it, including boxes
|
|
42
|
+
that cross the antimeridian. Pure Python, zero dependencies, fully typed.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install bboxkit # zero dependencies
|
|
46
|
+
pip install "bboxkit[pyproj]" # optional: reproject to any CRS
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
>>> from bboxkit import BBox, parse
|
|
51
|
+
>>> box = parse("29.0,40.9,29.2,41.1", order="wsen") # Istanbul
|
|
52
|
+
>>> box.to_overpass()
|
|
53
|
+
'(40.9,29,41.1,29.2)'
|
|
54
|
+
>>> box.to_gdal_te()
|
|
55
|
+
'-te 29 40.9 29.2 41.1'
|
|
56
|
+
>>> round(box.area_km2(), 3) # exact on the WGS 84 ellipsoid
|
|
57
|
+
373.741
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Every tool agrees that a bounding box has four numbers. Almost nothing else is agreed:
|
|
61
|
+
GeoJSON and Shapely write `west,south,east,north`, Overpass and Leaflet write
|
|
62
|
+
`south,west,north,east`, `gdal_translate -projwin` wants `west,north,east,south`, Nominatim
|
|
63
|
+
returns `[south, north, west, east]` as strings, and a box around Fiji has `west > east`.
|
|
64
|
+
bboxkit is a small, dependency-free value type that knows all of these conventions, so other
|
|
65
|
+
packages can depend on it instead of re-implementing (and re-debugging) them.
|
|
66
|
+
|
|
67
|
+
## Supported formats
|
|
68
|
+
|
|
69
|
+
| Format | Example | Parse | Format |
|
|
70
|
+
| --- | --- | --- | --- |
|
|
71
|
+
| W,S,E,N (GeoJSON, Shapely, OSM API, STAC) | `29,40.9,29.2,41.1` | `parse(s, order="wsen")` | `to_wsen()` |
|
|
72
|
+
| S,W,N,E (lat-first) | `40.9,29,41.1,29.2` | `parse(s, order="swne")` | `to_swne()` |
|
|
73
|
+
| bboxfinder.com | `http://bboxfinder.com/#40.9,29,41.1,29.2` | auto | `to_bboxfinder()` |
|
|
74
|
+
| Overpass QL | `(40.9,29,41.1,29.2)`, `[bbox:40.9,29,41.1,29.2]` | auto | `to_overpass()` |
|
|
75
|
+
| OSM URL | `...?bbox=W,S,E,N`, `?minlon=...`, `#map=14/41.0/29.0` | auto (`viewport=`) | `to_osm_url()` |
|
|
76
|
+
| WKT / EWKT | `POLYGON ((...))`, `SRID=3857;MULTIPOINT(...)`, any geometry | auto | `to_wkt()` |
|
|
77
|
+
| ENVELOPE (CQL, Solr) | `ENVELOPE(29, 29.2, 41.1, 40.9)` | auto | `to_wkt_envelope()` |
|
|
78
|
+
| PostGIS | `ST_MakeEnvelope(29, 40.9, 29.2, 41.1, 4326)`, `BOX(29 40.9,29.2 41.1)` | auto | `to_postgis()` |
|
|
79
|
+
| GeoJSON | `bbox` member, or computed from any geometry / Feature / FeatureCollection | auto | `to_geojson()` |
|
|
80
|
+
| Shapely / GeoPandas | `geom.bounds`, `gdf.total_bounds`, `__geo_interface__` | `parse(obj)` | `to_shapely_bounds()` |
|
|
81
|
+
| 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()` |
|
|
82
|
+
| Leaflet / Google | `[[40.9, 29], [41.1, 29.2]]` | `parse_leaflet()` | `to_leaflet()` |
|
|
83
|
+
| Mapbox / MapLibre | `[[29, 40.9], [29.2, 41.1]]` | `parse_mapbox()` | `to_mapbox()` |
|
|
84
|
+
| XYZ tile | `14/9375/6142`, `https://tile.../14/9375/6142.png` | auto | `to_tile()` |
|
|
85
|
+
| Quadkey (Bing) | `1202102332221212` | auto | `to_quadkey()` |
|
|
86
|
+
| Geohash | `sxk9` | auto | `to_geohash()` |
|
|
87
|
+
| Plus Code (OLC) | `8FVC9G8F+6W` | auto | `to_pluscode()` |
|
|
88
|
+
| MGRS square | `35TPF`, `31U DQ 48251 11932` | auto | `to_mgrs()` |
|
|
89
|
+
| Dictionaries | Nominatim result, ArcGIS extent (`wkid`), STAC collection, Leaflet `_southWest`, Google `southwest`, `{minx, ...}` | `parse(d)` | `to_dict()` |
|
|
90
|
+
| Place name | `"Istanbul"` | `nominatim.geocode_bbox()` (opt-in, network) | |
|
|
91
|
+
|
|
92
|
+
For cell formats (tile, quadkey, geohash, Plus Code, MGRS) parsing gives the cell's box and
|
|
93
|
+
formatting gives the **smallest cell that contains the whole box**.
|
|
94
|
+
|
|
95
|
+
Not included, on purpose: H3 (needs the icosahedral projection, i.e. the `h3` library) and
|
|
96
|
+
the polar UPS part of MGRS.
|
|
97
|
+
|
|
98
|
+
## One-liners
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from bboxkit import BBox, parse, union_all
|
|
102
|
+
|
|
103
|
+
parse("http://bboxfinder.com/#40.9,29,41.1,29.2") # BBox(29.0, 40.9, 29.2, 41.1)
|
|
104
|
+
parse("[bbox:40.9,29,41.1,29.2]") # Overpass setting
|
|
105
|
+
parse("gdalwarp -t_srs EPSG:32635 -te 500000 4500000 510000 4510000 in.tif out.tif")
|
|
106
|
+
parse("https://www.openstreetmap.org/#map=14/41.0082/28.9784", viewport=(1920, 1080))
|
|
107
|
+
parse("SRID=3857;POLYGON((0 0, 10 0, 10 10, 0 0))") # crs='EPSG:3857'
|
|
108
|
+
parse({"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[179, 0], [-179, 1]]}})
|
|
109
|
+
parse({"boundingbox": ["40.9", "41.1", "29.0", "29.2"]}) # a Nominatim result
|
|
110
|
+
parse(shapely_polygon) # uses .bounds
|
|
111
|
+
parse("14/9375/6142"); parse("sxk9"); parse("8FVC9G8F+6W"); parse("35TPF")
|
|
112
|
+
|
|
113
|
+
box = BBox(29.0, 40.9, 29.2, 41.1)
|
|
114
|
+
box.to_wkt() # 'POLYGON ((29 40.9, 29.2 40.9, 29.2 41.1, 29 41.1, 29 40.9))'
|
|
115
|
+
box.to_geojson() # {'type': 'Polygon', 'coordinates': [...], 'bbox': [...]}
|
|
116
|
+
box.to_leaflet() # [[40.9, 29.0], [41.1, 29.2]]
|
|
117
|
+
box.to_gdal_projwin() # '-projwin 29 41.1 29.2 40.9'
|
|
118
|
+
box.to_osm_url() # 'https://www.openstreetmap.org/#map=11/41.00008/29.10000'
|
|
119
|
+
box.format("postgis") # 'ST_MakeEnvelope(29, 40.9, 29.2, 41.1, 4326)'
|
|
120
|
+
box.to_geohash(), box.to_tile(), box.to_mgrs() # ('sxk', Tile(x=2, y=1, z=2), '35TPF')
|
|
121
|
+
|
|
122
|
+
box.area_km2() # 373.741450485826 (exact for a lat/lon rectangle on WGS 84)
|
|
123
|
+
box.center # (29.1, 41.0)
|
|
124
|
+
box.width_m, box.height_m # (16827.0..., 22210.7...)
|
|
125
|
+
box.buffer(500) # grown by 500 m on every side
|
|
126
|
+
box.buffer(-500) # shrunk by 500 m
|
|
127
|
+
box.contains((29.1, 41.0)); box.intersects(other); box & other; box | other
|
|
128
|
+
box.grid(4, 3) # 12 cells, row by row from the north-west
|
|
129
|
+
box.split(1000) # ~1 km x 1 km cells
|
|
130
|
+
box.tiles(14) # [Tile(x=9511, y=6136, z=14), ...]
|
|
131
|
+
box.tile_count(14) # 130
|
|
132
|
+
box.expand_to_aspect(16 / 9) # fit a 16:9 web map (Mercator), never shrinks
|
|
133
|
+
BBox(29.03, 40.91, 29.17, 41.08).snap(0.1) # BBox(29.0, 40.9, 29.2, 41.1)
|
|
134
|
+
round(box, 2)
|
|
135
|
+
box.to_crs("EPSG:3857") # built in
|
|
136
|
+
box.to_utm() # built in: EPSG:32635 for Istanbul
|
|
137
|
+
box.to_crs("EPSG:3035") # any CRS with the [pyproj] extra
|
|
138
|
+
w, s, e, n = box # unpacks like a tuple
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## The antimeridian
|
|
142
|
+
|
|
143
|
+
For longitude/latitude boxes, `west > east` means the box crosses the 180th meridian, exactly
|
|
144
|
+
as in RFC 7946:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
>>> fiji = BBox(177, -20, -178, -16)
|
|
148
|
+
>>> fiji.crosses_antimeridian, fiji.width, fiji.center
|
|
149
|
+
(True, 5.0, (179.5, -18.0))
|
|
150
|
+
>>> fiji.contains((-179.5, -18))
|
|
151
|
+
True
|
|
152
|
+
>>> fiji.parts()
|
|
153
|
+
(BBox(177.0, -20.0, 180.0, -16.0), BBox(-180.0, -20.0, -178.0, -16.0))
|
|
154
|
+
>>> BBox(170, 0, 175, 1) | BBox(-175, 0, -170, 1)
|
|
155
|
+
BBox(170.0, 0.0, -170.0, 1.0)
|
|
156
|
+
>>> fiji.to_geojson()["type"] # split into two parts, as RFC 7946 recommends
|
|
157
|
+
'MultiPolygon'
|
|
158
|
+
>>> fiji.to_leaflet() # one contiguous box on a web map
|
|
159
|
+
[[-20.0, 177.0], [-16.0, 182.0]]
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
- `union()` / `union_all()` and bounding boxes computed from GeoJSON or WKT use the smallest
|
|
163
|
+
longitude range that covers everything (the complement of the largest gap), so the RFC 7946
|
|
164
|
+
Fiji example gives `[177, -20, -178, -16]`, not `[-178, -20, 177, -16]`.
|
|
165
|
+
- A line or ring whose consecutive vertices are more than 180 degrees apart is read as crossing
|
|
166
|
+
the antimeridian (the shorter way round).
|
|
167
|
+
- `intersection()` returns `None`, one box, or raises if the overlap is two separate pieces
|
|
168
|
+
(wide boxes overlapping at both ends); `intersection_parts()` always returns the list.
|
|
169
|
+
- Formats that cannot express a crossing box (`-te`, `-projwin`, `ST_MakeEnvelope`, Shapely
|
|
170
|
+
bounds) raise a `ValueError` that points to `parts()`.
|
|
171
|
+
- In Web Mercator a crossing box keeps contiguous x values (east beyond 20037508.34 m) and
|
|
172
|
+
converts back to a crossing box.
|
|
173
|
+
|
|
174
|
+
## W,S,E,N or S,W,N,E?
|
|
175
|
+
|
|
176
|
+
Four bare numbers are ambiguous. bboxkit reads them as `west,south,east,north` unless only the
|
|
177
|
+
lat-first reading is valid, and emits an `AmbiguousOrderWarning` whenever both readings are
|
|
178
|
+
possible. Say what you mean to make the warning go away:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
parse("40.9,29,41.1,29.2", order="swne")
|
|
182
|
+
parse("swne:40.9,29,41.1,29.2")
|
|
183
|
+
parse(text, fmt="leaflet")
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Area, buffers and grids
|
|
187
|
+
|
|
188
|
+
- `area_km2()` uses the closed-form area of a region bounded by two meridians and two parallels
|
|
189
|
+
on the WGS 84 ellipsoid (via the authalic latitude function). It matches a densely densified
|
|
190
|
+
`pyproj.Geod` polygon to ~1e-11. 1 x 1 degree at the equator is 12308.4639 km2, at 60 N
|
|
191
|
+
6123.1409 km2; the whole globe is 510065621.724 km2.
|
|
192
|
+
- `buffer(m)` uses the local radii of curvature; the longitude margin is taken at the most
|
|
193
|
+
poleward latitude, so every point within `m` metres of the box is inside the result. Boxes
|
|
194
|
+
that reach a pole get the full longitude range.
|
|
195
|
+
- `split(cell_m)` builds a regular lon/lat grid whose cells measure `cell_m` at the box's central
|
|
196
|
+
latitude; `grid(nx, ny)` splits evenly. Both work across the antimeridian.
|
|
197
|
+
- Projected boxes (UTM, any metric CRS) do all of this in CRS units.
|
|
198
|
+
|
|
199
|
+
## Reprojection
|
|
200
|
+
|
|
201
|
+
`to_crs()` transforms densified edges (21 points per edge by default) and takes the extent.
|
|
202
|
+
WGS 84, Web Mercator (`EPSG:3857` and its aliases 900913, 3785, 102100) and WGS 84 / UTM
|
|
203
|
+
(`EPSG:326xx`, `EPSG:327xx`, ETRS89 `EPSG:258xx`) are built in; UTM uses the 6th-order Krueger
|
|
204
|
+
series (agrees with PROJ to nanometres). Anything else goes to pyproj when it is installed.
|
|
205
|
+
`to_utm()` picks the zone of the centre, including the Norway and Svalbard exceptions.
|
|
206
|
+
|
|
207
|
+
## Command line
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
$ bboxkit convert "29.0,40.9,29.2,41.1" --to overpass
|
|
211
|
+
bboxkit: warning: 29,40.9,29.2,41.1 is ambiguous; read as west,south,east,north. ...
|
|
212
|
+
(40.9,29,41.1,29.2)
|
|
213
|
+
$ bboxkit convert "29.0,40.9,29.2,41.1" --order wsen --to all
|
|
214
|
+
$ bboxkit convert "http://bboxfinder.com/#40.9,29,41.1,29.2" --to gdal-projwin
|
|
215
|
+
$ bboxkit area "29.0,40.9,29.2,41.1" --order wsen
|
|
216
|
+
373.741
|
|
217
|
+
$ bboxkit tiles "29.0,40.9,29.2,41.1" --order wsen --zoom 14 --count
|
|
218
|
+
130
|
|
219
|
+
$ bboxkit tiles "29.0,40.9,29.2,41.1" --order wsen --zoom 14 --format quadkey
|
|
220
|
+
$ bboxkit split "29.0,40.9,29.2,41.1" --order wsen --cell 1km --to geojson > grid.geojson
|
|
221
|
+
$ bboxkit split "29.0,40.9,29.2,41.1" --order wsen --grid 4x3
|
|
222
|
+
$ bboxkit buffer "POINT(29 41)" --distance 2km --to wkt
|
|
223
|
+
$ bboxkit reproject "29.0,40.9,29.2,41.1" --order wsen --to-crs utm
|
|
224
|
+
$ bboxkit info "177,-20,-178,-16" --order wsen
|
|
225
|
+
$ bboxkit geocode "Kadikoy, Istanbul" --user-agent "my-app/1.0 (me@example.org)"
|
|
226
|
+
$ bboxkit formats
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Boxes that start with a minus sign (`-122.5,37.7,-122.3,37.8`, `-te 1 2 3 4`) work as they are;
|
|
230
|
+
`-` reads the box from standard input.
|
|
231
|
+
|
|
232
|
+
## Place names (optional, network)
|
|
233
|
+
|
|
234
|
+
`parse()` never touches the network. Place lookup is a separate, explicit call:
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
from bboxkit.nominatim import geocode_bbox
|
|
238
|
+
|
|
239
|
+
geocode_bbox("Kadikoy, Istanbul", user_agent="my-app/1.0 (me@example.org)", timeout=10)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
It follows the [Nominatim usage policy](https://operations.osmfoundation.org/policies/nominatim/):
|
|
243
|
+
at most one request per second per process, an identifying User-Agent (please set your own),
|
|
244
|
+
results cached in memory, and a `base_url=` for your own Nominatim instance when you need
|
|
245
|
+
more than occasional lookups. Results are (c) OpenStreetMap contributors, ODbL.
|
|
246
|
+
|
|
247
|
+
## Design
|
|
248
|
+
|
|
249
|
+
- `BBox` is immutable, hashable, picklable and slotted. `==` and `hash` are exact so boxes can
|
|
250
|
+
be dict keys; `equals(other, tol=1e-9)` compares with a tolerance (longitudes modulo 360).
|
|
251
|
+
- Validation on construction: finite numbers, `south <= north`, latitudes and longitudes in
|
|
252
|
+
range for geographic CRSs, `west <= east` for projected ones. `BBox.wrap()` accepts unwrapped
|
|
253
|
+
longitudes such as `170 .. 190`.
|
|
254
|
+
- CRS strings are normalised (`4326`, `"epsg:4326"`, URNs, OGC URLs, `CRS84` all become
|
|
255
|
+
`"EPSG:4326"`).
|
|
256
|
+
- Cell lookups (`to_geohash()`, `to_pluscode()`, ...) use exact integer arithmetic where the
|
|
257
|
+
format allows it, so round trips are stable.
|
|
258
|
+
|
|
259
|
+
## Development
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
pip install -e ".[dev,pyproj]"
|
|
263
|
+
pytest --cov=bboxkit
|
|
264
|
+
ruff check . && ruff format --check src tests
|
|
265
|
+
mypy --strict src
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
MIT, see [LICENSE](LICENSE). Copyright (c) 2026 Yusuf Eminoğlu.
|
bboxkit-0.1.0/README.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# bboxkit
|
|
2
|
+
|
|
3
|
+
**Bounding boxes, without the guesswork.** Parse a bbox from whatever tool handed it to you,
|
|
4
|
+
convert it to whatever the next tool wants, and do correct geometry on it, including boxes
|
|
5
|
+
that cross the antimeridian. Pure Python, zero dependencies, fully typed.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install bboxkit # zero dependencies
|
|
9
|
+
pip install "bboxkit[pyproj]" # optional: reproject to any CRS
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
>>> from bboxkit import BBox, parse
|
|
14
|
+
>>> box = parse("29.0,40.9,29.2,41.1", order="wsen") # Istanbul
|
|
15
|
+
>>> box.to_overpass()
|
|
16
|
+
'(40.9,29,41.1,29.2)'
|
|
17
|
+
>>> box.to_gdal_te()
|
|
18
|
+
'-te 29 40.9 29.2 41.1'
|
|
19
|
+
>>> round(box.area_km2(), 3) # exact on the WGS 84 ellipsoid
|
|
20
|
+
373.741
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Every tool agrees that a bounding box has four numbers. Almost nothing else is agreed:
|
|
24
|
+
GeoJSON and Shapely write `west,south,east,north`, Overpass and Leaflet write
|
|
25
|
+
`south,west,north,east`, `gdal_translate -projwin` wants `west,north,east,south`, Nominatim
|
|
26
|
+
returns `[south, north, west, east]` as strings, and a box around Fiji has `west > east`.
|
|
27
|
+
bboxkit is a small, dependency-free value type that knows all of these conventions, so other
|
|
28
|
+
packages can depend on it instead of re-implementing (and re-debugging) them.
|
|
29
|
+
|
|
30
|
+
## Supported formats
|
|
31
|
+
|
|
32
|
+
| Format | Example | Parse | Format |
|
|
33
|
+
| --- | --- | --- | --- |
|
|
34
|
+
| W,S,E,N (GeoJSON, Shapely, OSM API, STAC) | `29,40.9,29.2,41.1` | `parse(s, order="wsen")` | `to_wsen()` |
|
|
35
|
+
| S,W,N,E (lat-first) | `40.9,29,41.1,29.2` | `parse(s, order="swne")` | `to_swne()` |
|
|
36
|
+
| bboxfinder.com | `http://bboxfinder.com/#40.9,29,41.1,29.2` | auto | `to_bboxfinder()` |
|
|
37
|
+
| Overpass QL | `(40.9,29,41.1,29.2)`, `[bbox:40.9,29,41.1,29.2]` | auto | `to_overpass()` |
|
|
38
|
+
| OSM URL | `...?bbox=W,S,E,N`, `?minlon=...`, `#map=14/41.0/29.0` | auto (`viewport=`) | `to_osm_url()` |
|
|
39
|
+
| WKT / EWKT | `POLYGON ((...))`, `SRID=3857;MULTIPOINT(...)`, any geometry | auto | `to_wkt()` |
|
|
40
|
+
| ENVELOPE (CQL, Solr) | `ENVELOPE(29, 29.2, 41.1, 40.9)` | auto | `to_wkt_envelope()` |
|
|
41
|
+
| PostGIS | `ST_MakeEnvelope(29, 40.9, 29.2, 41.1, 4326)`, `BOX(29 40.9,29.2 41.1)` | auto | `to_postgis()` |
|
|
42
|
+
| GeoJSON | `bbox` member, or computed from any geometry / Feature / FeatureCollection | auto | `to_geojson()` |
|
|
43
|
+
| Shapely / GeoPandas | `geom.bounds`, `gdf.total_bounds`, `__geo_interface__` | `parse(obj)` | `to_shapely_bounds()` |
|
|
44
|
+
| 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()` |
|
|
45
|
+
| Leaflet / Google | `[[40.9, 29], [41.1, 29.2]]` | `parse_leaflet()` | `to_leaflet()` |
|
|
46
|
+
| Mapbox / MapLibre | `[[29, 40.9], [29.2, 41.1]]` | `parse_mapbox()` | `to_mapbox()` |
|
|
47
|
+
| XYZ tile | `14/9375/6142`, `https://tile.../14/9375/6142.png` | auto | `to_tile()` |
|
|
48
|
+
| Quadkey (Bing) | `1202102332221212` | auto | `to_quadkey()` |
|
|
49
|
+
| Geohash | `sxk9` | auto | `to_geohash()` |
|
|
50
|
+
| Plus Code (OLC) | `8FVC9G8F+6W` | auto | `to_pluscode()` |
|
|
51
|
+
| MGRS square | `35TPF`, `31U DQ 48251 11932` | auto | `to_mgrs()` |
|
|
52
|
+
| Dictionaries | Nominatim result, ArcGIS extent (`wkid`), STAC collection, Leaflet `_southWest`, Google `southwest`, `{minx, ...}` | `parse(d)` | `to_dict()` |
|
|
53
|
+
| Place name | `"Istanbul"` | `nominatim.geocode_bbox()` (opt-in, network) | |
|
|
54
|
+
|
|
55
|
+
For cell formats (tile, quadkey, geohash, Plus Code, MGRS) parsing gives the cell's box and
|
|
56
|
+
formatting gives the **smallest cell that contains the whole box**.
|
|
57
|
+
|
|
58
|
+
Not included, on purpose: H3 (needs the icosahedral projection, i.e. the `h3` library) and
|
|
59
|
+
the polar UPS part of MGRS.
|
|
60
|
+
|
|
61
|
+
## One-liners
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from bboxkit import BBox, parse, union_all
|
|
65
|
+
|
|
66
|
+
parse("http://bboxfinder.com/#40.9,29,41.1,29.2") # BBox(29.0, 40.9, 29.2, 41.1)
|
|
67
|
+
parse("[bbox:40.9,29,41.1,29.2]") # Overpass setting
|
|
68
|
+
parse("gdalwarp -t_srs EPSG:32635 -te 500000 4500000 510000 4510000 in.tif out.tif")
|
|
69
|
+
parse("https://www.openstreetmap.org/#map=14/41.0082/28.9784", viewport=(1920, 1080))
|
|
70
|
+
parse("SRID=3857;POLYGON((0 0, 10 0, 10 10, 0 0))") # crs='EPSG:3857'
|
|
71
|
+
parse({"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[179, 0], [-179, 1]]}})
|
|
72
|
+
parse({"boundingbox": ["40.9", "41.1", "29.0", "29.2"]}) # a Nominatim result
|
|
73
|
+
parse(shapely_polygon) # uses .bounds
|
|
74
|
+
parse("14/9375/6142"); parse("sxk9"); parse("8FVC9G8F+6W"); parse("35TPF")
|
|
75
|
+
|
|
76
|
+
box = BBox(29.0, 40.9, 29.2, 41.1)
|
|
77
|
+
box.to_wkt() # 'POLYGON ((29 40.9, 29.2 40.9, 29.2 41.1, 29 41.1, 29 40.9))'
|
|
78
|
+
box.to_geojson() # {'type': 'Polygon', 'coordinates': [...], 'bbox': [...]}
|
|
79
|
+
box.to_leaflet() # [[40.9, 29.0], [41.1, 29.2]]
|
|
80
|
+
box.to_gdal_projwin() # '-projwin 29 41.1 29.2 40.9'
|
|
81
|
+
box.to_osm_url() # 'https://www.openstreetmap.org/#map=11/41.00008/29.10000'
|
|
82
|
+
box.format("postgis") # 'ST_MakeEnvelope(29, 40.9, 29.2, 41.1, 4326)'
|
|
83
|
+
box.to_geohash(), box.to_tile(), box.to_mgrs() # ('sxk', Tile(x=2, y=1, z=2), '35TPF')
|
|
84
|
+
|
|
85
|
+
box.area_km2() # 373.741450485826 (exact for a lat/lon rectangle on WGS 84)
|
|
86
|
+
box.center # (29.1, 41.0)
|
|
87
|
+
box.width_m, box.height_m # (16827.0..., 22210.7...)
|
|
88
|
+
box.buffer(500) # grown by 500 m on every side
|
|
89
|
+
box.buffer(-500) # shrunk by 500 m
|
|
90
|
+
box.contains((29.1, 41.0)); box.intersects(other); box & other; box | other
|
|
91
|
+
box.grid(4, 3) # 12 cells, row by row from the north-west
|
|
92
|
+
box.split(1000) # ~1 km x 1 km cells
|
|
93
|
+
box.tiles(14) # [Tile(x=9511, y=6136, z=14), ...]
|
|
94
|
+
box.tile_count(14) # 130
|
|
95
|
+
box.expand_to_aspect(16 / 9) # fit a 16:9 web map (Mercator), never shrinks
|
|
96
|
+
BBox(29.03, 40.91, 29.17, 41.08).snap(0.1) # BBox(29.0, 40.9, 29.2, 41.1)
|
|
97
|
+
round(box, 2)
|
|
98
|
+
box.to_crs("EPSG:3857") # built in
|
|
99
|
+
box.to_utm() # built in: EPSG:32635 for Istanbul
|
|
100
|
+
box.to_crs("EPSG:3035") # any CRS with the [pyproj] extra
|
|
101
|
+
w, s, e, n = box # unpacks like a tuple
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## The antimeridian
|
|
105
|
+
|
|
106
|
+
For longitude/latitude boxes, `west > east` means the box crosses the 180th meridian, exactly
|
|
107
|
+
as in RFC 7946:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
>>> fiji = BBox(177, -20, -178, -16)
|
|
111
|
+
>>> fiji.crosses_antimeridian, fiji.width, fiji.center
|
|
112
|
+
(True, 5.0, (179.5, -18.0))
|
|
113
|
+
>>> fiji.contains((-179.5, -18))
|
|
114
|
+
True
|
|
115
|
+
>>> fiji.parts()
|
|
116
|
+
(BBox(177.0, -20.0, 180.0, -16.0), BBox(-180.0, -20.0, -178.0, -16.0))
|
|
117
|
+
>>> BBox(170, 0, 175, 1) | BBox(-175, 0, -170, 1)
|
|
118
|
+
BBox(170.0, 0.0, -170.0, 1.0)
|
|
119
|
+
>>> fiji.to_geojson()["type"] # split into two parts, as RFC 7946 recommends
|
|
120
|
+
'MultiPolygon'
|
|
121
|
+
>>> fiji.to_leaflet() # one contiguous box on a web map
|
|
122
|
+
[[-20.0, 177.0], [-16.0, 182.0]]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
- `union()` / `union_all()` and bounding boxes computed from GeoJSON or WKT use the smallest
|
|
126
|
+
longitude range that covers everything (the complement of the largest gap), so the RFC 7946
|
|
127
|
+
Fiji example gives `[177, -20, -178, -16]`, not `[-178, -20, 177, -16]`.
|
|
128
|
+
- A line or ring whose consecutive vertices are more than 180 degrees apart is read as crossing
|
|
129
|
+
the antimeridian (the shorter way round).
|
|
130
|
+
- `intersection()` returns `None`, one box, or raises if the overlap is two separate pieces
|
|
131
|
+
(wide boxes overlapping at both ends); `intersection_parts()` always returns the list.
|
|
132
|
+
- Formats that cannot express a crossing box (`-te`, `-projwin`, `ST_MakeEnvelope`, Shapely
|
|
133
|
+
bounds) raise a `ValueError` that points to `parts()`.
|
|
134
|
+
- In Web Mercator a crossing box keeps contiguous x values (east beyond 20037508.34 m) and
|
|
135
|
+
converts back to a crossing box.
|
|
136
|
+
|
|
137
|
+
## W,S,E,N or S,W,N,E?
|
|
138
|
+
|
|
139
|
+
Four bare numbers are ambiguous. bboxkit reads them as `west,south,east,north` unless only the
|
|
140
|
+
lat-first reading is valid, and emits an `AmbiguousOrderWarning` whenever both readings are
|
|
141
|
+
possible. Say what you mean to make the warning go away:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
parse("40.9,29,41.1,29.2", order="swne")
|
|
145
|
+
parse("swne:40.9,29,41.1,29.2")
|
|
146
|
+
parse(text, fmt="leaflet")
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Area, buffers and grids
|
|
150
|
+
|
|
151
|
+
- `area_km2()` uses the closed-form area of a region bounded by two meridians and two parallels
|
|
152
|
+
on the WGS 84 ellipsoid (via the authalic latitude function). It matches a densely densified
|
|
153
|
+
`pyproj.Geod` polygon to ~1e-11. 1 x 1 degree at the equator is 12308.4639 km2, at 60 N
|
|
154
|
+
6123.1409 km2; the whole globe is 510065621.724 km2.
|
|
155
|
+
- `buffer(m)` uses the local radii of curvature; the longitude margin is taken at the most
|
|
156
|
+
poleward latitude, so every point within `m` metres of the box is inside the result. Boxes
|
|
157
|
+
that reach a pole get the full longitude range.
|
|
158
|
+
- `split(cell_m)` builds a regular lon/lat grid whose cells measure `cell_m` at the box's central
|
|
159
|
+
latitude; `grid(nx, ny)` splits evenly. Both work across the antimeridian.
|
|
160
|
+
- Projected boxes (UTM, any metric CRS) do all of this in CRS units.
|
|
161
|
+
|
|
162
|
+
## Reprojection
|
|
163
|
+
|
|
164
|
+
`to_crs()` transforms densified edges (21 points per edge by default) and takes the extent.
|
|
165
|
+
WGS 84, Web Mercator (`EPSG:3857` and its aliases 900913, 3785, 102100) and WGS 84 / UTM
|
|
166
|
+
(`EPSG:326xx`, `EPSG:327xx`, ETRS89 `EPSG:258xx`) are built in; UTM uses the 6th-order Krueger
|
|
167
|
+
series (agrees with PROJ to nanometres). Anything else goes to pyproj when it is installed.
|
|
168
|
+
`to_utm()` picks the zone of the centre, including the Norway and Svalbard exceptions.
|
|
169
|
+
|
|
170
|
+
## Command line
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
$ bboxkit convert "29.0,40.9,29.2,41.1" --to overpass
|
|
174
|
+
bboxkit: warning: 29,40.9,29.2,41.1 is ambiguous; read as west,south,east,north. ...
|
|
175
|
+
(40.9,29,41.1,29.2)
|
|
176
|
+
$ bboxkit convert "29.0,40.9,29.2,41.1" --order wsen --to all
|
|
177
|
+
$ bboxkit convert "http://bboxfinder.com/#40.9,29,41.1,29.2" --to gdal-projwin
|
|
178
|
+
$ bboxkit area "29.0,40.9,29.2,41.1" --order wsen
|
|
179
|
+
373.741
|
|
180
|
+
$ bboxkit tiles "29.0,40.9,29.2,41.1" --order wsen --zoom 14 --count
|
|
181
|
+
130
|
|
182
|
+
$ bboxkit tiles "29.0,40.9,29.2,41.1" --order wsen --zoom 14 --format quadkey
|
|
183
|
+
$ bboxkit split "29.0,40.9,29.2,41.1" --order wsen --cell 1km --to geojson > grid.geojson
|
|
184
|
+
$ bboxkit split "29.0,40.9,29.2,41.1" --order wsen --grid 4x3
|
|
185
|
+
$ bboxkit buffer "POINT(29 41)" --distance 2km --to wkt
|
|
186
|
+
$ bboxkit reproject "29.0,40.9,29.2,41.1" --order wsen --to-crs utm
|
|
187
|
+
$ bboxkit info "177,-20,-178,-16" --order wsen
|
|
188
|
+
$ bboxkit geocode "Kadikoy, Istanbul" --user-agent "my-app/1.0 (me@example.org)"
|
|
189
|
+
$ bboxkit formats
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Boxes that start with a minus sign (`-122.5,37.7,-122.3,37.8`, `-te 1 2 3 4`) work as they are;
|
|
193
|
+
`-` reads the box from standard input.
|
|
194
|
+
|
|
195
|
+
## Place names (optional, network)
|
|
196
|
+
|
|
197
|
+
`parse()` never touches the network. Place lookup is a separate, explicit call:
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
from bboxkit.nominatim import geocode_bbox
|
|
201
|
+
|
|
202
|
+
geocode_bbox("Kadikoy, Istanbul", user_agent="my-app/1.0 (me@example.org)", timeout=10)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
It follows the [Nominatim usage policy](https://operations.osmfoundation.org/policies/nominatim/):
|
|
206
|
+
at most one request per second per process, an identifying User-Agent (please set your own),
|
|
207
|
+
results cached in memory, and a `base_url=` for your own Nominatim instance when you need
|
|
208
|
+
more than occasional lookups. Results are (c) OpenStreetMap contributors, ODbL.
|
|
209
|
+
|
|
210
|
+
## Design
|
|
211
|
+
|
|
212
|
+
- `BBox` is immutable, hashable, picklable and slotted. `==` and `hash` are exact so boxes can
|
|
213
|
+
be dict keys; `equals(other, tol=1e-9)` compares with a tolerance (longitudes modulo 360).
|
|
214
|
+
- Validation on construction: finite numbers, `south <= north`, latitudes and longitudes in
|
|
215
|
+
range for geographic CRSs, `west <= east` for projected ones. `BBox.wrap()` accepts unwrapped
|
|
216
|
+
longitudes such as `170 .. 190`.
|
|
217
|
+
- CRS strings are normalised (`4326`, `"epsg:4326"`, URNs, OGC URLs, `CRS84` all become
|
|
218
|
+
`"EPSG:4326"`).
|
|
219
|
+
- Cell lookups (`to_geohash()`, `to_pluscode()`, ...) use exact integer arithmetic where the
|
|
220
|
+
format allows it, so round trips are stable.
|
|
221
|
+
|
|
222
|
+
## Development
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
pip install -e ".[dev,pyproj]"
|
|
226
|
+
pytest --cov=bboxkit
|
|
227
|
+
ruff check . && ruff format --check src tests
|
|
228
|
+
mypy --strict src
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
MIT, see [LICENSE](LICENSE). Copyright (c) 2026 Yusuf Eminoğlu.
|