osmfeatures 0.7.2__tar.gz → 0.7.4__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.
- osmfeatures-0.7.4/.cursor/rules/maplark-mcp-dogfood.mdc +12 -0
- osmfeatures-0.7.4/AUTO_TILES.md +50 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/PKG-INFO +40 -11
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/README.md +39 -10
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/pyproject.toml +1 -1
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/src/osmfeatures/__init__.py +6 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/src/osmfeatures/_http.py +3 -2
- osmfeatures-0.7.4/src/osmfeatures/_places.py +192 -0
- osmfeatures-0.7.4/src/osmfeatures/_query.py +250 -0
- osmfeatures-0.7.4/src/osmfeatures/_split.py +392 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/src/osmfeatures/async_client.py +150 -89
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/src/osmfeatures/chunking.py +19 -5
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/src/osmfeatures/cli.py +124 -11
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/src/osmfeatures/client.py +178 -283
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/src/osmfeatures/convenience.py +1 -1
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/src/osmfeatures/mcp/AGENT_INSTRUCTIONS.md +45 -45
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/src/osmfeatures/mcp/_mcp_session.py +108 -16
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/src/osmfeatures/mcp/_preview.py +259 -54
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/src/osmfeatures/mcp/mcp_server.py +50 -7
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/src/osmfeatures/models.py +99 -4
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/README.md +2 -1
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/conftest.py +24 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/example_apps/test_city_cycling_infrastructure.py +2 -2
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/example_apps/test_cycling_trails.py +2 -2
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/example_apps/test_geo_agent.py +1 -1
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/example_apps/test_geometry_filters.py +6 -6
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/example_apps/test_lakeside_ice_cream_hunt.py +2 -2
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/example_apps/test_park_bench_finder.py +1 -1
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/example_apps/test_park_explorer.py +1 -1
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/example_apps/test_pedestrian_shortest_path.py +1 -1
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/example_apps/test_pedestrian_wavefront_bfs.py +1 -1
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/example_apps/test_restaurant_guide.py +1 -1
- osmfeatures-0.7.4/tests/example_apps/test_split_until_fit.py +247 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/mcp/test_mcp_http.py +1 -1
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/mcp/test_mcp_server.py +86 -13
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/mcp/test_mcp_session.py +168 -15
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/mcp/test_preview.py +91 -1
- osmfeatures-0.7.4/tests/test_async_client.py +763 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/test_chunking.py +11 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/test_cli.py +59 -2
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/test_client.py +45 -2
- osmfeatures-0.7.4/tests/test_geo_agent_client.py +822 -0
- osmfeatures-0.7.4/tests/test_pagination.py +489 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/test_retry.py +6 -5
- osmfeatures-0.7.4/tests/test_split.py +130 -0
- osmfeatures-0.7.4/tmp/preview_redraw.pid +1 -0
- osmfeatures-0.7.4/tmp/preview_redraw.url +1 -0
- osmfeatures-0.7.4/tmp/preview_sweden_buildings.pid +1 -0
- osmfeatures-0.7.4/tmp/sweden_amenities_combined.json +1 -0
- osmfeatures-0.7.4/uv.lock +2179 -0
- osmfeatures-0.7.2/tests/test_async_client.py +0 -343
- osmfeatures-0.7.2/tests/test_geo_agent_client.py +0 -185
- osmfeatures-0.7.2/tests/test_pagination.py +0 -176
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/.github/copilot-instructions.md +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/.github/instructions/osmgeojson-python-conventions.instructions.md +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/.gitignore +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/Dockerfile +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/LICENSE +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/Makefile +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/src/osmfeatures/_geo_agent.py +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/src/osmfeatures/_pagination.py +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/src/osmfeatures/geometry.py +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/src/osmfeatures/mcp/__init__.py +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/src/osmfeatures/mcp/_geocode.py +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/src/osmfeatures/nearest.py +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/src/osmfeatures/output.py +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/src/osmfeatures/py.typed +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/src/osmfeatures/retry.py +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/.env.example +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/__init__.py +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/example_apps/__init__.py +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/example_apps/conftest.py +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/example_apps/graph_utils.py +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/example_apps/test_bike_path_dijkstra_liljeholmen_to_djurgarden.py +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/mcp/test_geocode.py +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/test_http.py +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/test_nearest_within.py +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/test_output.py +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/tutorial/conftest.py +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/tutorial/osm_features_query.py +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/tutorial/places_nearby.py +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/tutorial/places_search.py +0 -0
- {osmfeatures-0.7.2 → osmfeatures-0.7.4}/tests/tutorial/test_tutorial.py +0 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Prefer MapLark MCP for map and density demos in this repo
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# MapLark MCP in osmfeatures-py
|
|
7
|
+
|
|
8
|
+
When the user asks for a map, heatmap, density view, or live OSM visualization—and MapLark MCP is connected—use those MCP tools (`density`, `preview_map`, `places_search`, etc.), not the local CLI (`osmfeatures …`), shell scripts, or `PreviewServer` in this codebase.
|
|
9
|
+
|
|
10
|
+
Use the SDK/CLI/local preview only when the user is clearly developing or debugging the client (tests, CLI flags, MCP server code).
|
|
11
|
+
|
|
12
|
+
Geo flow for heatmaps: `density` → `preview_map` on the returned `collection_id`. Country or region: `geocode` first for bbox when the user named a place.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# auto_split tiling
|
|
2
|
+
|
|
3
|
+
Code: `iter_split_tiles` in `src/osmfeatures/_split.py`. Same walk for `query(auto_split=True)` and `places_search(auto_split=True)`.
|
|
4
|
+
|
|
5
|
+
The API will not return one huge bbox. Two different things can go wrong, so we use two different probes.
|
|
6
|
+
|
|
7
|
+
## Two problems, two probes
|
|
8
|
+
|
|
9
|
+
**Too many matches (density).** Inner Stockholm can be a small rectangle with 200k buildings. One fetch is only allowed `max_limit` rows (from `GET /v1/account/tier`). If we fetch first, the server 400s `result_too_large` and we have learned nothing except “too many.”
|
|
10
|
+
|
|
11
|
+
So we call `GET /v2/osm_features/count` first. That returns `{total: N}` with no geometries. If `N` is over the cap, we split the bbox into enough tiles that each *would* hold at most the cap, assuming matches are spread evenly. Then we count (and maybe fetch) each smaller tile. Empty tiles (`total == 0`) are skipped.
|
|
12
|
+
|
|
13
|
+
**Too big a rectangle (area).** Greater Stockholm cafes can be a tiny `total` and still fail: the bbox is larger than one search/query is allowed to scan (free tier example: 0.04 square degrees). Count does not tell you that. Count is allowed a much bigger window (~400 deg²).
|
|
14
|
+
|
|
15
|
+
So we do **not** split on a count-area error. That error looks like `count bbox area … limit 400`. The `400` is how big a **count** may be, not how big a fetch may be. Jumping to 400 deg² tiles would still 400 on every fetch.
|
|
16
|
+
|
|
17
|
+
We split on a **fetch** area error instead: `bbox area 5.0 exceeds the tagged tier limit 0.04`. The `0.04` is the real tile size. We remember it and jump the rest of the bbox to that size in one shot, instead of trying every quarter.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
density → count.total vs max_limit → split before fetch
|
|
21
|
+
area → fetch 400 “bbox area … Y” → split to tiles of size Y
|
|
22
|
+
not area → count 400 “count bbox area” → give up (window too big to count)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## The algorithm
|
|
26
|
+
|
|
27
|
+
Queue starts with the requested bbox (or `bbox_tiles` pieces if the caller already split). Cap: 32 tiles.
|
|
28
|
+
|
|
29
|
+
For each tile:
|
|
30
|
+
|
|
31
|
+
1. If we already know the fetch area cap `Y`, and this tile is larger than `Y`, split it to that size and skip count/fetch.
|
|
32
|
+
2. Count the tile.
|
|
33
|
+
- `total == 0`: skip.
|
|
34
|
+
- `total` over the row cap: replace with a power-of-2 grid (`ceil(total / cap)`, rounded up to 2, 4, 8, …). Do this at most twice (the original tile, then one leftover dense child). Need more than 32 tiles, or a third split: `OSMFeaturesTooDenseError`.
|
|
35
|
+
- Count 400 `count bbox area`: re-raise. Do not split.
|
|
36
|
+
- Count 400 that names the **fetch** cap (`bbox area … limit Y`, no `count` prefix): remember `Y` and jump.
|
|
37
|
+
3. Fetch the tile (`GET /v3/osm_features` or `POST /v1/places/search`).
|
|
38
|
+
- Fetch 400 `bbox area … Y`: remember `Y`, jump, do not probe every quarter.
|
|
39
|
+
- Fetch 400 `result_too_large` after a count that already fit: `OSMFeaturesTooDenseError`. Count was wrong; splitting again would thrash.
|
|
40
|
+
- Success: keep the page.
|
|
41
|
+
|
|
42
|
+
Covering the bbox would need more than 32 tiles (usually an area jump): `OSMFeaturesTooManyTilesError`. That is “the window is too large,” not “too crowded.” Shrink the named place. Do not keep bisecting.
|
|
43
|
+
|
|
44
|
+
Results from tiles are merged and deduped by feature `id` (tiles share edges).
|
|
45
|
+
|
|
46
|
+
`limit=10` is a page size. It truncates; it does not lower the density cap. 20 matches with `limit=10` stay one tile.
|
|
47
|
+
|
|
48
|
+
`bbox_tiles=4` without `auto_split` just fetches those 4 tiles. No count, no second jump.
|
|
49
|
+
|
|
50
|
+
Hours on `places_search` (`as_of` / `open_now`) use a 10k parse/scan cap instead of `max_limit`. See `_places.py`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: osmfeatures
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.4
|
|
4
4
|
Summary: Official client for the MapLark OpenStreetMap Features API (GeoJSON, FlatGeobuf, GeoParquet, CSV)
|
|
5
5
|
Project-URL: Homepage, https://maplark.com
|
|
6
6
|
Project-URL: Repository, https://github.com/MapLark/osmfeatures-python
|
|
@@ -54,6 +54,7 @@ Official Python client for the [MapLark OSM Features API](https://maplark.com) w
|
|
|
54
54
|
- [Query OSM features](#query-osm-features)
|
|
55
55
|
- [Query a large bbox](#query-a-large-bbox)
|
|
56
56
|
- [Count](#count)
|
|
57
|
+
- [Density](#density)
|
|
57
58
|
- [Async client](#async-client)
|
|
58
59
|
- [Convenience helpers](#convenience-helpers)
|
|
59
60
|
- [Usage](#usage)
|
|
@@ -145,7 +146,7 @@ with OSMFeaturesClient(api_key="sk-...") as client:
|
|
|
145
146
|
|
|
146
147
|
### Query OSM features
|
|
147
148
|
|
|
148
|
-
`query()` calls `GET /v3/osm_features` and returns the whole tile. Omit `limit` for the API default. Paid keys may raise `limit` up to their `max_limit
|
|
149
|
+
`query()` calls `GET /v3/osm_features` and returns the whole tile. Omit `limit` for the API default. Paid keys may raise `limit` up to their `max_limit`. A larger match set is truncated (`X-Has-More: true`). Pass `auto_split=True` to read the key's `max_limit` from `GET /v1/account/tier`, count first with a scalar `GET /v2/osm_features/count` (no `group_by`), and jump to a power-of-2 tile grid when the match set exceeds that cap (one extra jump if a leftover leaf is still dense). That adds latency. Pass `bbox_tiles=2` (or 4, 8, ...) to split the bbox up front. `query()` does not take `cursor`.
|
|
149
150
|
|
|
150
151
|
```python
|
|
151
152
|
fc = client.query(
|
|
@@ -168,7 +169,7 @@ Common filters:
|
|
|
168
169
|
- `way_shape="polygon" | "line" | "all"` (omit = both shapes; `all` also means both)
|
|
169
170
|
- `clip_geometry=True | False` (omit for the API default; set `False` to keep full geometry outside bbox)
|
|
170
171
|
- `bbox_tiles=2` (power of 2) to split a bbox that exceeds your tier area cap
|
|
171
|
-
- `
|
|
172
|
+
- `auto_split=True` to count first, then fetch (reads `max_limit` from `GET /v1/account/tier`, cached on the client; jumps to a power-of-2 grid when the match set exceeds that cap; one extra jump if a leftover leaf is still dense; a lower `limit` truncates; jumps when a 400 names a bbox-area cap). Caps at 32 tiles (`OSMFeaturesTooManyTilesError` if covering the bbox needs more, `OSMFeaturesTooDenseError` if a tile still overflows). `OSMFeaturesTimeoutError` is only a real wall-clock miss.
|
|
172
173
|
- `accept="text/csv"` / `"application/flatgeobuf"` / `"application/vnd.apache.parquet"` (returns `BinaryQueryResult`)
|
|
173
174
|
|
|
174
175
|
|
|
@@ -180,8 +181,7 @@ Use this to query a larger bbox than what is allowed by splitting the bbox up in
|
|
|
180
181
|
all_restaurants = client.query(
|
|
181
182
|
bbox="18.063,59.322,18.082,59.332",
|
|
182
183
|
tags="amenity=restaurant",
|
|
183
|
-
|
|
184
|
-
max_features=1_000_000,
|
|
184
|
+
auto_split=True,
|
|
185
185
|
timeout=60,
|
|
186
186
|
)
|
|
187
187
|
```
|
|
@@ -191,7 +191,7 @@ all_restaurants = client.query(
|
|
|
191
191
|
#### Count
|
|
192
192
|
Analyze feature counts with a histogram over a very large area - city and country sized bounding boxes allowed. For example, you can find out how many cafes, bars, and restaurants are in different cities or countries.
|
|
193
193
|
|
|
194
|
-
`client.count` calls `GET /v2/osm_features/count`. Example amenity histogram:
|
|
194
|
+
`client.count` calls `GET /v2/osm_features/count`. Omit `group_by` for a scalar total. Example amenity histogram:
|
|
195
195
|
|
|
196
196
|
```python
|
|
197
197
|
client.count(
|
|
@@ -205,6 +205,24 @@ client.count(
|
|
|
205
205
|
```
|
|
206
206
|
|
|
207
207
|
|
|
208
|
+
#### Density
|
|
209
|
+
|
|
210
|
+
`client.density` calls `GET /v2/osm_features/density`. Occupied grid bins come back as a GeoJSON FeatureCollection of points with `properties.count`, plus `total` and `truncated`. These are not OSM features (no `osm_id` or tags). Omit `cell_deg` for 0.1. `type` omitted is nodes. `tags` or `or_tags` is required. `limit` is the max occupied cells (the key's `max_limit` when omitted).
|
|
211
|
+
|
|
212
|
+
A Europe box (`-26,33,52,72`, about 3042 deg²) fits the density area cap (4000 deg²) and the 80° span. It is above `max_units_per_request` on every non-enterprise tier, so pass `disable_budget_warning=True` to run it. The same box still 400s on count (area cap 400 deg²). A unit-cap 400 on count can use that flag the same way. A bbox-area 400 cannot.
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
client.density(
|
|
216
|
+
bbox="-26,33,52,72",
|
|
217
|
+
tags=["place"],
|
|
218
|
+
cell_deg=0.1,
|
|
219
|
+
disable_budget_warning=True,
|
|
220
|
+
)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
CLI: `osmfeatures density --bbox -26,33,52,72 --tags place --cell-deg 0.1 --disable-budget-warning`.
|
|
224
|
+
|
|
225
|
+
|
|
208
226
|
### Async client
|
|
209
227
|
|
|
210
228
|
Async methods mirror the sync API (`query_async`, `query_all_async`):
|
|
@@ -264,7 +282,7 @@ Results come back as summaries (ids, names, OSM tags, lon/lat scalars, `distance
|
|
|
264
282
|
- Places: `places_search`, `places_nearby`, `places_details`
|
|
265
283
|
- Geocode: `geocode` (Nominatim interim)
|
|
266
284
|
- Routes: `routes_isochrone`, `routes_path`, `routes_optimized_path`
|
|
267
|
-
- Generic OSM: `query`, `
|
|
285
|
+
- Generic OSM: `query`, `count` (histogram / total), `density` (grid bins; `preview_map` the id)
|
|
268
286
|
- Local (no HTTP): `nearest_within`, `pairs_within`, `filter_open`, `point_in_polygon`, `points_in_polygon`
|
|
269
287
|
- Draw / export: `preview_map`, `export_geojson`
|
|
270
288
|
|
|
@@ -328,6 +346,7 @@ Planner rules: you pick tags, bbox or location+radius, budgets, `openNow`/`asOf`
|
|
|
328
346
|
| "Cafes on a tour of Gamla Stan" | `places_search`, `filter_open` + `routes_optimized_path` (`loop=true`) |
|
|
329
347
|
| "Suggest a walk to a bar, a restaurant, and a cafe, no particular order" | `routes_optimized_path` with `loop=false` |
|
|
330
348
|
| "Is the office a 20-minute walk from the apartment?" | `routes_isochrone` from A, `point_in_polygon` for B |
|
|
349
|
+
| "Amenity heatmap for Sweden" | `geocode` (bbox), `density` (`amenity`; `disable_budget_warning` if the unit cap 400s), `preview_map` |
|
|
331
350
|
| "Show this on a map" | `preview_map(collection_id)` after a search or route |
|
|
332
351
|
|
|
333
352
|
The same operations exist on `OSMFeaturesClient` when you are not going through an LLM (see [Places and routes](#places-and-routes)).
|
|
@@ -343,7 +362,16 @@ Runnable Python chains live in `tests/example_apps/test_geo_agent.py`. Full HTTP
|
|
|
343
362
|
|
|
344
363
|
### Places search
|
|
345
364
|
|
|
346
|
-
`places_search()` finds places in a bounding box **or** a `location` plus `radius` (not both). Optional `tags` (AND) and `or_tags` (OR) use the same OSM filters as `query()`. Omit `limit` to use the API default (100
|
|
365
|
+
`places_search()` finds places in a bounding box **or** a `location` plus `radius` (not both). Optional `tags` (AND) and `or_tags` (OR) use the same OSM filters as `query()`. Omit `limit` to use the API default (100). Paid keys may raise `limit` up to their `/v3` `max_limit`; `asOf` / `openNow` still cap hours parse at 10_000.
|
|
366
|
+
|
|
367
|
+
`bbox_tiles` and `auto_split` **only apply to bbox search** (same names as `query()`). Pass `bbox_tiles=2` (or 4, 8, ...) to split a bbox up front. Pass `auto_split=True` to count first via `GET /v2/osm_features/count` with `way_shape=polygon`, then fetch. No hours: split on `total` vs the key's `max_limit`. With `asOf` / `openNow`: AND `opening_hours`. An `asOf` page `limit` (default 100) truncates; density-split vs the 10k parse cap only when `limit` is raised above 10k. `openNow` density-splits vs that 10k scan cap even with a small fill limit. A search that exceeds the tagged **fetch** area cap 400s; that 400 is split in one jump from the limit in the message, not by probing every quarter, up to 32 tiles. A `count bbox area` 400 is not split (the window is still too big to count). location+radius is a circle and cannot split. To tile a radius window, convert it first with `around_to_bbox` (a square that contains the circle; corner hits can fall outside the original radius):
|
|
368
|
+
|
|
369
|
+
```python
|
|
370
|
+
from osmfeatures import around_to_bbox
|
|
371
|
+
|
|
372
|
+
bbox = around_to_bbox(lon=18.075, lat=59.316, radius_m=800)
|
|
373
|
+
cafes = client.places_search(bbox=bbox, or_tags=["amenity=cafe"], auto_split=True)
|
|
374
|
+
```
|
|
347
375
|
|
|
348
376
|
```python
|
|
349
377
|
cafes = client.places_search(
|
|
@@ -357,7 +385,7 @@ print(len(cafes["features"]), "open cafes")
|
|
|
357
385
|
print(cafes["metadata"]["evaluated_at"])
|
|
358
386
|
```
|
|
359
387
|
|
|
360
|
-
Response is a GeoJSON FeatureCollection plus `metadata.evaluated_at` (UTC instant used for hours).
|
|
388
|
+
Response is a GeoJSON FeatureCollection plus `metadata.evaluated_at` (UTC instant used for hours) and `metadata.units` (credits charged). After `bbox_tiles` / `auto_split`, `units` is the sum of the search tiles.
|
|
361
389
|
|
|
362
390
|
### Nearby (ranked from a point)
|
|
363
391
|
|
|
@@ -375,7 +403,7 @@ for item in nearby["items"]:
|
|
|
375
403
|
print(item["distance_m"], item["feature"]["id"])
|
|
376
404
|
```
|
|
377
405
|
|
|
378
|
-
Response: `{status, items: [{feature, distance_m}],
|
|
406
|
+
Response: `{status, items: [{feature, distance_m}], units, evaluated_at}`.
|
|
379
407
|
|
|
380
408
|
### Place details
|
|
381
409
|
|
|
@@ -388,7 +416,7 @@ print(details["feature"]["properties"]["tags"])
|
|
|
388
416
|
print(details["timezone"], details["evaluated_at"])
|
|
389
417
|
```
|
|
390
418
|
|
|
391
|
-
Response: `{status, feature,
|
|
419
|
+
Response: `{status, feature, units, evaluated_at, timezone}`. Hours are annotated at request time in the place's IANA zone (from its coordinates).
|
|
392
420
|
|
|
393
421
|
### Opening hours
|
|
394
422
|
|
|
@@ -499,6 +527,7 @@ The repository includes runnable example-app tests in `tests/example_apps/` show
|
|
|
499
527
|
- `test_bike_path_dijkstra_liljeholmen_to_djurgarden.py`: tiled corridor bike routing from Liljeholmen to Djurgarden.
|
|
500
528
|
- `test_geometry_filters.py`: zoom + area/length filters for large buildings and long roads.
|
|
501
529
|
- `test_geo_agent.py`: Python places/routes chains (bar crawl, bike parks, isochrone filter/compare/coverage, client-side open-at-clock).
|
|
530
|
+
- `test_auto_split.py`: ``auto_split`` for a city-scale cafe bbox (area cap) and dense inner-Stockholm buildings (``max_limit``).
|
|
502
531
|
|
|
503
532
|
Run all example apps:
|
|
504
533
|
|
|
@@ -11,6 +11,7 @@ Official Python client for the [MapLark OSM Features API](https://maplark.com) w
|
|
|
11
11
|
- [Query OSM features](#query-osm-features)
|
|
12
12
|
- [Query a large bbox](#query-a-large-bbox)
|
|
13
13
|
- [Count](#count)
|
|
14
|
+
- [Density](#density)
|
|
14
15
|
- [Async client](#async-client)
|
|
15
16
|
- [Convenience helpers](#convenience-helpers)
|
|
16
17
|
- [Usage](#usage)
|
|
@@ -102,7 +103,7 @@ with OSMFeaturesClient(api_key="sk-...") as client:
|
|
|
102
103
|
|
|
103
104
|
### Query OSM features
|
|
104
105
|
|
|
105
|
-
`query()` calls `GET /v3/osm_features` and returns the whole tile. Omit `limit` for the API default. Paid keys may raise `limit` up to their `max_limit
|
|
106
|
+
`query()` calls `GET /v3/osm_features` and returns the whole tile. Omit `limit` for the API default. Paid keys may raise `limit` up to their `max_limit`. A larger match set is truncated (`X-Has-More: true`). Pass `auto_split=True` to read the key's `max_limit` from `GET /v1/account/tier`, count first with a scalar `GET /v2/osm_features/count` (no `group_by`), and jump to a power-of-2 tile grid when the match set exceeds that cap (one extra jump if a leftover leaf is still dense). That adds latency. Pass `bbox_tiles=2` (or 4, 8, ...) to split the bbox up front. `query()` does not take `cursor`.
|
|
106
107
|
|
|
107
108
|
```python
|
|
108
109
|
fc = client.query(
|
|
@@ -125,7 +126,7 @@ Common filters:
|
|
|
125
126
|
- `way_shape="polygon" | "line" | "all"` (omit = both shapes; `all` also means both)
|
|
126
127
|
- `clip_geometry=True | False` (omit for the API default; set `False` to keep full geometry outside bbox)
|
|
127
128
|
- `bbox_tiles=2` (power of 2) to split a bbox that exceeds your tier area cap
|
|
128
|
-
- `
|
|
129
|
+
- `auto_split=True` to count first, then fetch (reads `max_limit` from `GET /v1/account/tier`, cached on the client; jumps to a power-of-2 grid when the match set exceeds that cap; one extra jump if a leftover leaf is still dense; a lower `limit` truncates; jumps when a 400 names a bbox-area cap). Caps at 32 tiles (`OSMFeaturesTooManyTilesError` if covering the bbox needs more, `OSMFeaturesTooDenseError` if a tile still overflows). `OSMFeaturesTimeoutError` is only a real wall-clock miss.
|
|
129
130
|
- `accept="text/csv"` / `"application/flatgeobuf"` / `"application/vnd.apache.parquet"` (returns `BinaryQueryResult`)
|
|
130
131
|
|
|
131
132
|
|
|
@@ -137,8 +138,7 @@ Use this to query a larger bbox than what is allowed by splitting the bbox up in
|
|
|
137
138
|
all_restaurants = client.query(
|
|
138
139
|
bbox="18.063,59.322,18.082,59.332",
|
|
139
140
|
tags="amenity=restaurant",
|
|
140
|
-
|
|
141
|
-
max_features=1_000_000,
|
|
141
|
+
auto_split=True,
|
|
142
142
|
timeout=60,
|
|
143
143
|
)
|
|
144
144
|
```
|
|
@@ -148,7 +148,7 @@ all_restaurants = client.query(
|
|
|
148
148
|
#### Count
|
|
149
149
|
Analyze feature counts with a histogram over a very large area - city and country sized bounding boxes allowed. For example, you can find out how many cafes, bars, and restaurants are in different cities or countries.
|
|
150
150
|
|
|
151
|
-
`client.count` calls `GET /v2/osm_features/count`. Example amenity histogram:
|
|
151
|
+
`client.count` calls `GET /v2/osm_features/count`. Omit `group_by` for a scalar total. Example amenity histogram:
|
|
152
152
|
|
|
153
153
|
```python
|
|
154
154
|
client.count(
|
|
@@ -162,6 +162,24 @@ client.count(
|
|
|
162
162
|
```
|
|
163
163
|
|
|
164
164
|
|
|
165
|
+
#### Density
|
|
166
|
+
|
|
167
|
+
`client.density` calls `GET /v2/osm_features/density`. Occupied grid bins come back as a GeoJSON FeatureCollection of points with `properties.count`, plus `total` and `truncated`. These are not OSM features (no `osm_id` or tags). Omit `cell_deg` for 0.1. `type` omitted is nodes. `tags` or `or_tags` is required. `limit` is the max occupied cells (the key's `max_limit` when omitted).
|
|
168
|
+
|
|
169
|
+
A Europe box (`-26,33,52,72`, about 3042 deg²) fits the density area cap (4000 deg²) and the 80° span. It is above `max_units_per_request` on every non-enterprise tier, so pass `disable_budget_warning=True` to run it. The same box still 400s on count (area cap 400 deg²). A unit-cap 400 on count can use that flag the same way. A bbox-area 400 cannot.
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
client.density(
|
|
173
|
+
bbox="-26,33,52,72",
|
|
174
|
+
tags=["place"],
|
|
175
|
+
cell_deg=0.1,
|
|
176
|
+
disable_budget_warning=True,
|
|
177
|
+
)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
CLI: `osmfeatures density --bbox -26,33,52,72 --tags place --cell-deg 0.1 --disable-budget-warning`.
|
|
181
|
+
|
|
182
|
+
|
|
165
183
|
### Async client
|
|
166
184
|
|
|
167
185
|
Async methods mirror the sync API (`query_async`, `query_all_async`):
|
|
@@ -221,7 +239,7 @@ Results come back as summaries (ids, names, OSM tags, lon/lat scalars, `distance
|
|
|
221
239
|
- Places: `places_search`, `places_nearby`, `places_details`
|
|
222
240
|
- Geocode: `geocode` (Nominatim interim)
|
|
223
241
|
- Routes: `routes_isochrone`, `routes_path`, `routes_optimized_path`
|
|
224
|
-
- Generic OSM: `query`, `
|
|
242
|
+
- Generic OSM: `query`, `count` (histogram / total), `density` (grid bins; `preview_map` the id)
|
|
225
243
|
- Local (no HTTP): `nearest_within`, `pairs_within`, `filter_open`, `point_in_polygon`, `points_in_polygon`
|
|
226
244
|
- Draw / export: `preview_map`, `export_geojson`
|
|
227
245
|
|
|
@@ -285,6 +303,7 @@ Planner rules: you pick tags, bbox or location+radius, budgets, `openNow`/`asOf`
|
|
|
285
303
|
| "Cafes on a tour of Gamla Stan" | `places_search`, `filter_open` + `routes_optimized_path` (`loop=true`) |
|
|
286
304
|
| "Suggest a walk to a bar, a restaurant, and a cafe, no particular order" | `routes_optimized_path` with `loop=false` |
|
|
287
305
|
| "Is the office a 20-minute walk from the apartment?" | `routes_isochrone` from A, `point_in_polygon` for B |
|
|
306
|
+
| "Amenity heatmap for Sweden" | `geocode` (bbox), `density` (`amenity`; `disable_budget_warning` if the unit cap 400s), `preview_map` |
|
|
288
307
|
| "Show this on a map" | `preview_map(collection_id)` after a search or route |
|
|
289
308
|
|
|
290
309
|
The same operations exist on `OSMFeaturesClient` when you are not going through an LLM (see [Places and routes](#places-and-routes)).
|
|
@@ -300,7 +319,16 @@ Runnable Python chains live in `tests/example_apps/test_geo_agent.py`. Full HTTP
|
|
|
300
319
|
|
|
301
320
|
### Places search
|
|
302
321
|
|
|
303
|
-
`places_search()` finds places in a bounding box **or** a `location` plus `radius` (not both). Optional `tags` (AND) and `or_tags` (OR) use the same OSM filters as `query()`. Omit `limit` to use the API default (100
|
|
322
|
+
`places_search()` finds places in a bounding box **or** a `location` plus `radius` (not both). Optional `tags` (AND) and `or_tags` (OR) use the same OSM filters as `query()`. Omit `limit` to use the API default (100). Paid keys may raise `limit` up to their `/v3` `max_limit`; `asOf` / `openNow` still cap hours parse at 10_000.
|
|
323
|
+
|
|
324
|
+
`bbox_tiles` and `auto_split` **only apply to bbox search** (same names as `query()`). Pass `bbox_tiles=2` (or 4, 8, ...) to split a bbox up front. Pass `auto_split=True` to count first via `GET /v2/osm_features/count` with `way_shape=polygon`, then fetch. No hours: split on `total` vs the key's `max_limit`. With `asOf` / `openNow`: AND `opening_hours`. An `asOf` page `limit` (default 100) truncates; density-split vs the 10k parse cap only when `limit` is raised above 10k. `openNow` density-splits vs that 10k scan cap even with a small fill limit. A search that exceeds the tagged **fetch** area cap 400s; that 400 is split in one jump from the limit in the message, not by probing every quarter, up to 32 tiles. A `count bbox area` 400 is not split (the window is still too big to count). location+radius is a circle and cannot split. To tile a radius window, convert it first with `around_to_bbox` (a square that contains the circle; corner hits can fall outside the original radius):
|
|
325
|
+
|
|
326
|
+
```python
|
|
327
|
+
from osmfeatures import around_to_bbox
|
|
328
|
+
|
|
329
|
+
bbox = around_to_bbox(lon=18.075, lat=59.316, radius_m=800)
|
|
330
|
+
cafes = client.places_search(bbox=bbox, or_tags=["amenity=cafe"], auto_split=True)
|
|
331
|
+
```
|
|
304
332
|
|
|
305
333
|
```python
|
|
306
334
|
cafes = client.places_search(
|
|
@@ -314,7 +342,7 @@ print(len(cafes["features"]), "open cafes")
|
|
|
314
342
|
print(cafes["metadata"]["evaluated_at"])
|
|
315
343
|
```
|
|
316
344
|
|
|
317
|
-
Response is a GeoJSON FeatureCollection plus `metadata.evaluated_at` (UTC instant used for hours).
|
|
345
|
+
Response is a GeoJSON FeatureCollection plus `metadata.evaluated_at` (UTC instant used for hours) and `metadata.units` (credits charged). After `bbox_tiles` / `auto_split`, `units` is the sum of the search tiles.
|
|
318
346
|
|
|
319
347
|
### Nearby (ranked from a point)
|
|
320
348
|
|
|
@@ -332,7 +360,7 @@ for item in nearby["items"]:
|
|
|
332
360
|
print(item["distance_m"], item["feature"]["id"])
|
|
333
361
|
```
|
|
334
362
|
|
|
335
|
-
Response: `{status, items: [{feature, distance_m}],
|
|
363
|
+
Response: `{status, items: [{feature, distance_m}], units, evaluated_at}`.
|
|
336
364
|
|
|
337
365
|
### Place details
|
|
338
366
|
|
|
@@ -345,7 +373,7 @@ print(details["feature"]["properties"]["tags"])
|
|
|
345
373
|
print(details["timezone"], details["evaluated_at"])
|
|
346
374
|
```
|
|
347
375
|
|
|
348
|
-
Response: `{status, feature,
|
|
376
|
+
Response: `{status, feature, units, evaluated_at, timezone}`. Hours are annotated at request time in the place's IANA zone (from its coordinates).
|
|
349
377
|
|
|
350
378
|
### Opening hours
|
|
351
379
|
|
|
@@ -456,6 +484,7 @@ The repository includes runnable example-app tests in `tests/example_apps/` show
|
|
|
456
484
|
- `test_bike_path_dijkstra_liljeholmen_to_djurgarden.py`: tiled corridor bike routing from Liljeholmen to Djurgarden.
|
|
457
485
|
- `test_geometry_filters.py`: zoom + area/length filters for large buildings and long roads.
|
|
458
486
|
- `test_geo_agent.py`: Python places/routes chains (bar crawl, bike parks, isochrone filter/compare/coverage, client-side open-at-clock).
|
|
487
|
+
- `test_auto_split.py`: ``auto_split`` for a city-scale cafe bbox (area cap) and dense inner-Stockholm buildings (``max_limit``).
|
|
459
488
|
|
|
460
489
|
Run all example apps:
|
|
461
490
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "osmfeatures"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.4"
|
|
8
8
|
description = "Official client for the MapLark OpenStreetMap Features API (GeoJSON, FlatGeobuf, GeoParquet, CSV)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -41,6 +41,7 @@ from .convenience import (
|
|
|
41
41
|
)
|
|
42
42
|
from .models import (
|
|
43
43
|
BinaryQueryResult,
|
|
44
|
+
DensityFeatureCollection,
|
|
44
45
|
OSMFeature,
|
|
45
46
|
OSMFeatureCollection,
|
|
46
47
|
OSMFeaturesAPIError,
|
|
@@ -49,6 +50,8 @@ from .models import (
|
|
|
49
50
|
OSMFeaturesForbiddenError,
|
|
50
51
|
OSMFeaturesRateLimitError,
|
|
51
52
|
OSMFeaturesTimeoutError,
|
|
53
|
+
OSMFeaturesTooDenseError,
|
|
54
|
+
OSMFeaturesTooManyTilesError,
|
|
52
55
|
ResponseMeta,
|
|
53
56
|
)
|
|
54
57
|
from .output import to_dataframe, to_geodataframe
|
|
@@ -63,6 +66,7 @@ __all__ = [
|
|
|
63
66
|
# Models
|
|
64
67
|
"OSMFeature",
|
|
65
68
|
"OSMFeatureCollection",
|
|
69
|
+
"DensityFeatureCollection",
|
|
66
70
|
"BinaryQueryResult",
|
|
67
71
|
"ResponseMeta",
|
|
68
72
|
# Exceptions
|
|
@@ -72,6 +76,8 @@ __all__ = [
|
|
|
72
76
|
"OSMFeaturesRateLimitError",
|
|
73
77
|
"OSMFeaturesAPIError",
|
|
74
78
|
"OSMFeaturesTimeoutError",
|
|
79
|
+
"OSMFeaturesTooDenseError",
|
|
80
|
+
"OSMFeaturesTooManyTilesError",
|
|
75
81
|
# Output
|
|
76
82
|
"to_dataframe",
|
|
77
83
|
"to_geodataframe",
|
|
@@ -14,6 +14,7 @@ from .models import (
|
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
DEFAULT_BASE_URL = "https://api.maplark.com"
|
|
17
|
+
ACCOUNT_TIER_PATH = "/v1/account/tier"
|
|
17
18
|
GEOJSON_ACCEPT = "application/geo+json"
|
|
18
19
|
DEFAULT_TIMEOUT = 60.0
|
|
19
20
|
_LOG_BODY_MAX = 4000
|
|
@@ -126,7 +127,7 @@ def raise_for_response(resp: Any) -> None:
|
|
|
126
127
|
body.get("detail", body.get("message", f"Rate limit exceeded (HTTP 429): {resp.text[:200]}")),
|
|
127
128
|
error_code=body.get("error", ""),
|
|
128
129
|
tier=body.get("tier", ""),
|
|
129
|
-
|
|
130
|
+
units=body.get("units"),
|
|
130
131
|
max_units_per_request=body.get("max_units_per_request"),
|
|
131
132
|
retry_after=retry_after,
|
|
132
133
|
)
|
|
@@ -184,7 +185,7 @@ def build_rate_limit_error(resp: Any) -> OSMFeaturesRateLimitError:
|
|
|
184
185
|
body.get("detail", body.get("message", "Rate limit exceeded")),
|
|
185
186
|
error_code=body.get("error", ""),
|
|
186
187
|
tier=body.get("tier", ""),
|
|
187
|
-
|
|
188
|
+
units=body.get("units"),
|
|
188
189
|
max_units_per_request=body.get("max_units_per_request"),
|
|
189
190
|
retry_after=retry_after,
|
|
190
191
|
)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Shared ``POST /v1/places/search`` tiling: bbox_tiles and auto_split.
|
|
2
|
+
|
|
3
|
+
Sync and async clients pass their POST/count callables into
|
|
4
|
+
:func:`execute_places_search`. ``bbox_tiles`` and ``auto_split`` are
|
|
5
|
+
client-side; they are not sent in the JSON body. Both only apply to bbox
|
|
6
|
+
search. location+radius is a circle (same as ``query`` radius) and cannot
|
|
7
|
+
split; convert with :func:`around_to_bbox` and search that bbox if
|
|
8
|
+
you need tiles.
|
|
9
|
+
|
|
10
|
+
``auto_split`` uses the same count-first walk as :func:`execute_query`.
|
|
11
|
+
Counts are ``GET /v2/osm_features/count`` with ``way_shape=polygon``.
|
|
12
|
+
``asOf`` / ``openNow`` AND ``opening_hours``. An ``asOf`` page ``limit``
|
|
13
|
+
truncates; density-split vs the 10k parse cap only when ``limit`` is
|
|
14
|
+
raised above 10k. ``openNow`` always density-splits vs that 10k scan cap
|
|
15
|
+
(the drain returns 200 at 10k hours-tagged rows).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from ._geo_agent import places_search_body
|
|
24
|
+
from ._pagination import DEFAULT_QUERY_ALL_TIMEOUT_S
|
|
25
|
+
from ._split import (
|
|
26
|
+
await_maybe,
|
|
27
|
+
fold_auto_split,
|
|
28
|
+
hours_parse_split_cap,
|
|
29
|
+
iter_split_tiles,
|
|
30
|
+
match_split_cap,
|
|
31
|
+
max_limit_from_tier,
|
|
32
|
+
run_sync,
|
|
33
|
+
)
|
|
34
|
+
from .chunking import merge_features
|
|
35
|
+
|
|
36
|
+
BBOX_TILES_REQUIRES_BBOX = (
|
|
37
|
+
"bbox_tiles only applies to bbox search, not location+radius"
|
|
38
|
+
)
|
|
39
|
+
AUTO_SPLIT_REQUIRES_BBOX = (
|
|
40
|
+
"auto_split only applies to bbox search, not location+radius"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
_SUM_META_KEYS = ("units", "input_count", "kept_count", "hours_parsed")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _merge_place_metadata(metas: list[dict[str, Any]]) -> dict[str, Any]:
|
|
48
|
+
"""Sum billed/scan fields across tiles. Keep first ``evaluated_at``."""
|
|
49
|
+
out = dict(metas[0])
|
|
50
|
+
out.pop("hint", None)
|
|
51
|
+
for key in _SUM_META_KEYS:
|
|
52
|
+
total = 0
|
|
53
|
+
seen = False
|
|
54
|
+
for meta in metas:
|
|
55
|
+
value = meta.get(key)
|
|
56
|
+
if value is None:
|
|
57
|
+
continue
|
|
58
|
+
try:
|
|
59
|
+
total += int(value)
|
|
60
|
+
except (TypeError, ValueError):
|
|
61
|
+
continue
|
|
62
|
+
seen = True
|
|
63
|
+
if seen:
|
|
64
|
+
out[key] = total
|
|
65
|
+
if any(m.get("hours_parse_capped") for m in metas):
|
|
66
|
+
out["hours_parse_capped"] = True
|
|
67
|
+
return out
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _merge_place_collections(payloads: list[dict[str, Any]]) -> dict[str, Any]:
|
|
71
|
+
if not payloads:
|
|
72
|
+
return {"type": "FeatureCollection", "features": []}
|
|
73
|
+
features = merge_features(
|
|
74
|
+
[list(p.get("features") or []) for p in payloads if isinstance(p, dict)]
|
|
75
|
+
)
|
|
76
|
+
out = dict(payloads[0]) if isinstance(payloads[0], dict) else {}
|
|
77
|
+
out["type"] = "FeatureCollection"
|
|
78
|
+
out["features"] = features
|
|
79
|
+
metas = [
|
|
80
|
+
p["metadata"]
|
|
81
|
+
for p in payloads
|
|
82
|
+
if isinstance(p, dict) and isinstance(p.get("metadata"), dict)
|
|
83
|
+
]
|
|
84
|
+
if metas:
|
|
85
|
+
out["metadata"] = _merge_place_metadata(metas)
|
|
86
|
+
return out
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _hours_on(open_now: bool, as_of: str | None) -> bool:
|
|
90
|
+
return bool(open_now or as_of is not None)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def execute_places_search(
|
|
94
|
+
*,
|
|
95
|
+
post: Callable[[dict[str, Any]], Any],
|
|
96
|
+
count: Callable[..., Any],
|
|
97
|
+
account_tier: Callable[..., Any] | None = None,
|
|
98
|
+
bbox: str | None = None,
|
|
99
|
+
location: dict[str, float] | None = None,
|
|
100
|
+
radius: float | None = None,
|
|
101
|
+
type: str | None = None, # noqa: A002
|
|
102
|
+
tags: list[str] | None = None,
|
|
103
|
+
or_tags: list[str] | None = None,
|
|
104
|
+
limit: int | None = None,
|
|
105
|
+
open_now: bool = False,
|
|
106
|
+
as_of: str | None = None,
|
|
107
|
+
bbox_tiles: int = 1,
|
|
108
|
+
auto_split: bool = False,
|
|
109
|
+
split_until_fit: bool | None = None,
|
|
110
|
+
timeout: float | None = DEFAULT_QUERY_ALL_TIMEOUT_S,
|
|
111
|
+
) -> dict[str, Any]:
|
|
112
|
+
"""POST places/search, optionally tiling *bbox* like :func:`execute_query`."""
|
|
113
|
+
auto_split = fold_auto_split(auto_split, split_until_fit)
|
|
114
|
+
if bbox_tiles != 1 and not isinstance(bbox, str):
|
|
115
|
+
raise ValueError(BBOX_TILES_REQUIRES_BBOX)
|
|
116
|
+
if auto_split and not isinstance(bbox, str):
|
|
117
|
+
raise ValueError(AUTO_SPLIT_REQUIRES_BBOX)
|
|
118
|
+
|
|
119
|
+
def _body(*, tile: str | None = None) -> dict[str, Any]:
|
|
120
|
+
return places_search_body(
|
|
121
|
+
bbox=tile if tile is not None else bbox,
|
|
122
|
+
location=location,
|
|
123
|
+
radius=radius,
|
|
124
|
+
type=type,
|
|
125
|
+
tags=tags,
|
|
126
|
+
or_tags=or_tags,
|
|
127
|
+
limit=limit,
|
|
128
|
+
open_now=open_now,
|
|
129
|
+
as_of=as_of,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if bbox_tiles == 1 and not auto_split:
|
|
133
|
+
return await await_maybe(post(_body())) # type: ignore[no-any-return]
|
|
134
|
+
|
|
135
|
+
assert isinstance(bbox, str)
|
|
136
|
+
|
|
137
|
+
count_kwargs: dict[str, Any] = {"way_shape": "polygon"}
|
|
138
|
+
if tags is not None:
|
|
139
|
+
count_kwargs["tags"] = list(tags)
|
|
140
|
+
if or_tags is not None:
|
|
141
|
+
count_kwargs["or_tags"] = list(or_tags)
|
|
142
|
+
hours_on = _hours_on(open_now, as_of)
|
|
143
|
+
if hours_on:
|
|
144
|
+
count_kwargs["tags"] = [*count_kwargs.get("tags", []), "opening_hours"]
|
|
145
|
+
omitted_cap = 1
|
|
146
|
+
if auto_split and not hours_on:
|
|
147
|
+
if account_tier is None:
|
|
148
|
+
raise TypeError("auto_split requires account_tier")
|
|
149
|
+
omitted_cap = max_limit_from_tier(await await_maybe(account_tier()))
|
|
150
|
+
tile_cap = (
|
|
151
|
+
hours_parse_split_cap(limit, open_now=open_now)
|
|
152
|
+
if hours_on
|
|
153
|
+
else match_split_cap(limit, omitted_cap)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
async def _fetch_tile(tile: str) -> dict[str, Any]:
|
|
157
|
+
return await await_maybe(post(_body(tile=tile))) # type: ignore[no-any-return]
|
|
158
|
+
|
|
159
|
+
async def _match_count(tile: str) -> int | None:
|
|
160
|
+
body = await await_maybe(count(bbox=tile, **count_kwargs))
|
|
161
|
+
return int(body["total"])
|
|
162
|
+
|
|
163
|
+
payloads: list[dict[str, Any]] = []
|
|
164
|
+
async for page in iter_split_tiles(
|
|
165
|
+
bbox,
|
|
166
|
+
bbox_tiles=bbox_tiles,
|
|
167
|
+
auto_split=auto_split,
|
|
168
|
+
timeout=timeout,
|
|
169
|
+
tile_cap=tile_cap,
|
|
170
|
+
match_count=_match_count if auto_split else None,
|
|
171
|
+
fetch_tile=_fetch_tile,
|
|
172
|
+
overflow_name="places_search",
|
|
173
|
+
):
|
|
174
|
+
payloads.append(page)
|
|
175
|
+
return _merge_place_collections(payloads)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def execute_places_search_sync(
|
|
179
|
+
*,
|
|
180
|
+
post: Callable[[dict[str, Any]], Any],
|
|
181
|
+
count: Callable[..., Any],
|
|
182
|
+
account_tier: Callable[..., Any] | None = None,
|
|
183
|
+
**kwargs: Any,
|
|
184
|
+
) -> dict[str, Any]:
|
|
185
|
+
"""Run :func:`execute_places_search` on a private loop (or a worker if nested)."""
|
|
186
|
+
return run_sync(
|
|
187
|
+
execute_places_search,
|
|
188
|
+
post=post,
|
|
189
|
+
count=count,
|
|
190
|
+
account_tier=account_tier,
|
|
191
|
+
**kwargs,
|
|
192
|
+
)
|