osmfeatures 0.2.3 → 0.3.0
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.
- package/README.md +98 -52
- package/dist/geojson-feature.js +14 -7
- package/dist/geometry.d.ts +12 -0
- package/dist/geometry.js +60 -0
- package/dist/index.d.ts +34 -7
- package/dist/index.js +66 -23
- package/dist/nearest.d.ts +28 -0
- package/dist/nearest.js +98 -0
- package/dist/preset/catalog.d.ts +2 -0
- package/dist/preset/catalog.js +4 -4
- package/dist/preset/resolve.d.ts +3 -1
- package/dist/preset/resolve.js +4 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,28 +1,41 @@
|
|
|
1
|
-
# OSM Features
|
|
1
|
+
# TypeScript OSM Features client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
npm package for the [MapLark OSM Features API](https://maplark.com). Fetch OpenStreetMap buildings, roads, parks, and POIs as GeoJSON, FlatGeobuf, GeoParquet, or CSV from Node.js or the browser without standing up Overpass or converting extracts by hand. Simply search by tag and bounding box or location + radius. The API keeps OSM semantics intact, like node, way, relation, and returns GeoJSON Features you can feed straight into Leaflet, MapLibre, OpenLayers, or any geospatial toolchain. Use this lib to build geospatial apps on OSM easily without hitting rate limits or setting up complex and expensive infrastructure yourself.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The backend is dedicated PostGIS, not the public Overpass endpoint, with API keys and rate limits so map tiles and POI queries stay fast under load. This SDK also covers local-search and mobility: amenity lookup, OSM opening hours, and walk or bicycle routing.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Contents
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
9
|
+
- [Quick start](#quick-start)
|
|
10
|
+
- [Functions and Parameters](#functions-and-parameters)
|
|
11
|
+
- [query()](#query)
|
|
12
|
+
- [query_all](#query_all)
|
|
13
|
+
- [estimate_cost](#estimate_cost)
|
|
14
|
+
- [usage](#usage)
|
|
15
|
+
- [Places and routes](#places-and-routes)
|
|
16
|
+
- [Places search](#places-search)
|
|
17
|
+
- [Nearby](#nearby-ranked-from-a-point)
|
|
18
|
+
- [Place details](#place-details)
|
|
19
|
+
- [Opening hours](#opening-hours)
|
|
20
|
+
- [X near Y](#x-near-y-local-join)
|
|
21
|
+
- [Walk and bike routes](#walk-and-bike-routes)
|
|
22
|
+
- [MCP server](#mcp-server)
|
|
12
23
|
|
|
13
|
-
|
|
24
|
+
OSM types map to GeoJSON the way GIS tools expect:
|
|
14
25
|
|
|
15
|
-
|
|
26
|
+
- `node` → Point
|
|
27
|
+
- `way` → LineString or Polygon
|
|
28
|
+
- `relation` → MultiPolygon or a bundle of geometries
|
|
16
29
|
|
|
17
|
-
|
|
18
|
-
- `shape=polygon` - closed ways (buildings, parks) or multipolygon relations.
|
|
19
|
-
- `shape=all` - both shapes (default when shape is omitted).
|
|
30
|
+
Filters use ordinary OSM tags (`amenity=cafe`, `building=yes`). If you already write Overpass or edit OSM, the same keys work here. Drop a FeatureCollection into Leaflet, MapLibre, OpenLayers, or Turf.
|
|
20
31
|
|
|
21
|
-
|
|
32
|
+
Use `way_shape` when you need lines vs areas:
|
|
22
33
|
|
|
23
|
-
`
|
|
34
|
+
- `way_shape=line` — unclosed ways (streets, footpaths, rivers) and line-like relations (routes, some boundaries)
|
|
35
|
+
- `way_shape=polygon` — closed ways (building footprints, parks) and multipolygon relations
|
|
36
|
+
- `way_shape=all` — both (the default if you leave it off)
|
|
24
37
|
|
|
25
|
-
|
|
38
|
+
Buildings in a box: `type=way & tags=building` — the same idea as Overpass `way[building]`.
|
|
26
39
|
|
|
27
40
|
# Quick start
|
|
28
41
|
|
|
@@ -102,7 +115,7 @@ Geometric filters such specific OSM element type, min length, or including centr
|
|
|
102
115
|
| Param | Type | Default | Description |
|
|
103
116
|
| ------------ | ---------------------- | ------- | --------------------------------------------------------------------------------------- |
|
|
104
117
|
| `type` | `string` | all | OSM element types, e.g. `node`, `way`, `relation`, or comma-separated (`way,relation`). |
|
|
105
|
-
| `
|
|
118
|
+
| `wayShape` | `line | polygon | all` | `all` | Geometry class for ways and relations. `shape` is a deprecated alias. |
|
|
106
119
|
| `centroid` | `boolean` | `false` | When `true`, include a centroid on non-point features. |
|
|
107
120
|
| `clipGeometry` | `boolean` | `true` | When `true`, clip returned geometry to the requested `bbox`. Set `false` for full geometry. |
|
|
108
121
|
| `minLengthM` | `number` | | Minimum length in metres (lines). |
|
|
@@ -121,7 +134,7 @@ Extra filters to for pagination, output format (accept),
|
|
|
121
134
|
| Param | Type | Default | Description |
|
|
122
135
|
| ---------------------- | --------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
123
136
|
| `accept` | `string` | `application/geo+json` | Response media type in header. Options - `application/geo+json`, `text/csv`, `text/tab-separated-values`, `application/flatgeobuf`, and `application/vnd.apache.parquet`. |
|
|
124
|
-
| `limit` | `number` | `1000`
|
|
137
|
+
| `limit` | `number` | API `1000` | Page size. Omit to use the API default. Max `6000`. |
|
|
125
138
|
| `cursor` | `string` | | Pagination cursor from a previous `meta.next_cursor`. |
|
|
126
139
|
| `disableBudgetWarning` | `boolean` | `false` | Ignore warnings for large queries that consume budget quotas. |
|
|
127
140
|
| `zoom` | `number` | | Map zoom hint (used by presets / server-side simplification policies). |
|
|
@@ -168,12 +181,12 @@ console.log(all.meta.page_count, all.meta.has_more, all.meta.units_charged);
|
|
|
168
181
|
|
|
169
182
|
### Params
|
|
170
183
|
|
|
171
|
-
Same filter params as `query` (`bbox`, `tags`, `orTags`, `notTags`, `type`, `
|
|
184
|
+
Same filter params as `query` (`bbox`, `tags`, `orTags`, `notTags`, `type`, `wayShape`, `zoom`, `location`, `radius`, `osmIds`, `minLengthM`, `maxLengthM`, `minAreaM2`, `maxAreaM2`, `centroid`, `clipGeometry`, `disableBudgetWarning`), plus:
|
|
172
185
|
|
|
173
186
|
|
|
174
187
|
| Param | Type | Default | Description |
|
|
175
188
|
| -------------- | --------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
176
|
-
| `limitPerPage` | `number` | `1000`
|
|
189
|
+
| `limitPerPage` | `number` | API `1000` | Upstream `limit` per HTTP request (page size). Omit to use the API default. |
|
|
177
190
|
| `bboxTiles` | `number` | `2` | Split `bbox` into this many tiles (must be a power of 2: `1`, `2`, `4`, `8`, …). Each tile is paginated separately, then features are merged and deduped. |
|
|
178
191
|
| `maxPages` | `number` | `15` | Max pages fetched **per tile**. |
|
|
179
192
|
| `maxFeatures` | `number | null` | `55000` | Cap on merged features after dedupe. Pass `null` for no cap. |
|
|
@@ -197,8 +210,9 @@ Same fields as `query`, plus:
|
|
|
197
210
|
| `relay_partial_reason` | e.g. `upstream_rejected_cursor` or `upstream_rate_limited_after_retries`. |
|
|
198
211
|
|
|
199
212
|
|
|
200
|
-
Also exports layer presets (`resolveLayerFromQuery`, `OSM_FEATURES_LAYER_PRESETS`, ...)
|
|
201
|
-
|
|
213
|
+
Also exports layer presets (`resolveLayerFromQuery`, `OSM_FEATURES_LAYER_PRESETS`, ...),
|
|
214
|
+
GeoJSON payload helpers (`featureCentroid`, `geometryBounds`, `parseFeatureId`, ...),
|
|
215
|
+
and local helpers (`nearest_within`, `point_in_geometry`, `isOpenNow`).
|
|
202
216
|
|
|
203
217
|
## `estimate_cost`
|
|
204
218
|
|
|
@@ -221,37 +235,15 @@ const usage = await client.usage();
|
|
|
221
235
|
console.log(usage.tier, usage.usage_this_month, usage.remaining_this_month);
|
|
222
236
|
```
|
|
223
237
|
|
|
224
|
-
##
|
|
238
|
+
## Places and routes
|
|
225
239
|
|
|
226
|
-
`query()` is the
|
|
240
|
+
`query()` is the raw OSM layer: footprints, highways, park polygons, any tag and geometry class. Places and routes sit on the same planet extract but answer product questions: amenities in a box, ranked POIs from a pin, opening hours, walk/bike isochrones, and multi-stop paths. You supply tags, extent, time, and `WALK` or `BICYCLE`. The API returns coordinates, `openNow`, distances, and network geometry.
|
|
227
241
|
|
|
228
|
-
|
|
242
|
+
Unset fields are omitted so server defaults apply (`places_search` limit 100, `places_nearby` radius 1000 m and limit 100, `loop` true). HTTP docs: [maplark.com/developer](https://maplark.com/developer).
|
|
229
243
|
|
|
230
|
-
|
|
231
|
-
| -------- | ---- | ------------ |
|
|
232
|
-
| `places_search` | `POST /v1/places/search` | Find places in a bounding box **or** a `location` plus `radius`. Filter with OSM tags. Optional `openNow` / `asOf` for opening hours. |
|
|
233
|
-
| `places_nearby` | `POST /v1/places/nearby` | "X near this point". Same tags and hours filters, ranked nearest-first by straight-line distance. |
|
|
234
|
-
| `places_details` | `GET /v1/places/{osm_type}/{osm_id}` | Reload one place by the id search or nearby returned (`node/123`). |
|
|
235
|
-
| `routes_isochrone` | `POST /v1/routes/isochrone` | Walk or bike reach polygon from an origin (how far you can get in N metres or seconds). |
|
|
236
|
-
| `routes_path` | `POST /v1/routes/path` | Walk or bike through stops in the order you list them. No reordering. |
|
|
237
|
-
| `routes_optimized_path` | `POST /v1/routes/optimized_path` | Order the stops for you (a tour from `start`). `loop` (default true) returns to start. |
|
|
244
|
+
### Places search
|
|
238
245
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
#### Typical questions
|
|
242
|
-
|
|
243
|
-
| Prompt | SDK |
|
|
244
|
-
|------|-----|
|
|
245
|
-
| "Cafes near me" | `client.places_nearby()` or `client.places_search()` with `location` + `radius` |
|
|
246
|
-
| "Restaurants within 150 m of a station" | two `client.places_search()` calls, then join locally by distance |
|
|
247
|
-
| "Bars open at 20:00" | `client.places_search()` with `asOf`, keep `openingHours.status == "open"` |
|
|
248
|
-
| "Cafes within a 10-minute bike ride" | `client.routes_isochrone()` + `client.places_search()` in a covering radius + keep points inside the polygon |
|
|
249
|
-
| "A walking bar crawl in Stockholm" | `client.places_search()` + `client.routes_optimized_path()` (`loop: true`) |
|
|
250
|
-
| "Walk from my hotel to the cafe, then the office" | `client.routes_path()` with those stops in listed order |
|
|
251
|
-
| "Suggest a walk to a bar, a restaurant, and a cafe, no particular order" | `client.routes_optimized_path()` with `loop: false` |
|
|
252
|
-
| "Is the office a 20-minute walk from the apartment?" | `client.routes_isochrone()` from A, point-in-polygon for B |
|
|
253
|
-
|
|
254
|
-
### Examples for `places_search` / `places_nearby` / `places_details`
|
|
246
|
+
`places_search()` looks up POIs inside a bounding box **or** around `{ lat, lon }` + `radius` (pick one). `tags` is AND; `orTags` is OR; both use the same OSM keys as `query()`. Leave `limit` off for the API default (100, max 10_000).
|
|
255
247
|
|
|
256
248
|
```ts
|
|
257
249
|
const origin = { lat: 59.316, lon: 18.075 };
|
|
@@ -263,7 +255,13 @@ const cafes = await client.places_search({
|
|
|
263
255
|
openNow: true,
|
|
264
256
|
asOf: '2026-08-10T18:00:00+02:00',
|
|
265
257
|
});
|
|
258
|
+
```
|
|
266
259
|
|
|
260
|
+
### Nearby (ranked from a point)
|
|
261
|
+
|
|
262
|
+
`places_nearby()` is “what is closest to this coordinate?”. You must pass `tags` or `orTags`. Hits are ordered by straight-line spheroid distance. Defaults if omitted: 1000 m radius, 100 results.
|
|
263
|
+
|
|
264
|
+
```ts
|
|
267
265
|
const nearby = await client.places_nearby({
|
|
268
266
|
location: origin,
|
|
269
267
|
orTags: ['amenity=cafe'],
|
|
@@ -271,16 +269,55 @@ const nearby = await client.places_nearby({
|
|
|
271
269
|
openNow: true,
|
|
272
270
|
asOf: '2026-08-10T18:00:00+02:00',
|
|
273
271
|
});
|
|
272
|
+
```
|
|
274
273
|
|
|
274
|
+
### Place details
|
|
275
|
+
|
|
276
|
+
`places_details()` reloads a single OSM place by the id search or nearby gave you (`node/123`), or as `{ osmType, osmId }`.
|
|
277
|
+
|
|
278
|
+
```ts
|
|
275
279
|
const first = (cafes.features as { id: string }[])[0];
|
|
276
280
|
const details = await client.places_details({ osmType: first.id });
|
|
281
|
+
// same as: client.places_details({ osmType: 'node', osmId: 123 })
|
|
277
282
|
```
|
|
278
283
|
|
|
279
|
-
|
|
284
|
+
Hours are evaluated at request time in that place’s timezone.
|
|
285
|
+
|
|
286
|
+
### Opening hours
|
|
287
|
+
|
|
288
|
+
When OSM `opening_hours` can be parsed, the feature gets `properties.openNow` as `true` or `false`. Missing or junk hours omit the field. `isOpenNow(feature)` keeps known-open places and unwraps the details `{ feature }` envelope.
|
|
289
|
+
|
|
290
|
+
Timezone is inferred from coordinates (IANA). There is no `timezone` request field.
|
|
291
|
+
|
|
292
|
+
- `openNow: true` drops closed and unknown-hours POIs (Google Places–style `openNow`).
|
|
293
|
+
- `asOf` is the evaluation instant (default: now). An offset (`Z`, `+02:00`) is an absolute instant. A naive `2026-08-10T20:00:00` is local clock at the search point or bbox centre.
|
|
294
|
+
- `asOf` or `openNow` also require an `opening_hours` tag, so untagged amenities do not pad the page.
|
|
295
|
+
- Places that are closed but tagged still appear unless `openNow` is set.
|
|
280
296
|
|
|
281
|
-
###
|
|
297
|
+
### "X near Y" (local join)
|
|
282
298
|
|
|
283
|
-
|
|
299
|
+
`places_nearby` ranks against one origin. “Restaurants within 150 m of a station” is two searches plus an in-process join. `nearest_within` does not hit the API.
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
import { nearest_within } from 'osmfeatures';
|
|
303
|
+
|
|
304
|
+
const bbox = '18.05,59.33,18.10,59.36';
|
|
305
|
+
const restaurants = await client.places_search({ bbox, orTags: ['amenity=restaurant'] });
|
|
306
|
+
const stations = await client.places_search({ bbox, orTags: ['railway=station'] });
|
|
307
|
+
const pairs = nearest_within(restaurants, stations, 150, { limit: 20 });
|
|
308
|
+
|
|
309
|
+
for (const pair of pairs) {
|
|
310
|
+
console.log(pair.distance_m, pair.feature, 'near', pair.nearest);
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Each pair is `{ feature, distance_m, nearest }`. The point is `geometry` when it is a Point, otherwise `properties.centroid` (same as `featureCentroid`). Neither present throws. Empty secondary → `[]`. Distances are spherical haversine (mean Earth radius 6371000 m). `limit` keeps the closest pairs (default 20); `{ limit: null }` returns every primary with a match. More than 500000 comparisons throws — lower `places_search` / `places_nearby` `limit`, do not use `query_all`. `{ data, meta }` from `query()` / `query_all()` is accepted.
|
|
315
|
+
|
|
316
|
+
### Walk and bike routes
|
|
317
|
+
|
|
318
|
+
Paths follow OSM walk and bicycle ways. Default `travelMode` is `WALK`; pass `'BICYCLE'` for bikes. Driving is not offered yet.
|
|
319
|
+
|
|
320
|
+
Coordinates take `lon` or `lng`. For `routes_isochrone`, set exactly one of `maxDistanceM` or `durationS`. `searchBufferM` widens the highway fetch if the default corridor cannot form a path.
|
|
284
321
|
|
|
285
322
|
```ts
|
|
286
323
|
const origin = { lon: 18.075, lat: 59.316 };
|
|
@@ -299,6 +336,15 @@ const tour = await client.routes_optimized_path({
|
|
|
299
336
|
start: origin,
|
|
300
337
|
stops: [cafe],
|
|
301
338
|
});
|
|
339
|
+
|
|
340
|
+
const office = { lon: 18.08, lat: 59.318 };
|
|
341
|
+
point_in_geometry(office.lon, office.lat, iso);
|
|
302
342
|
```
|
|
303
343
|
|
|
304
|
-
|
|
344
|
+
In-process (no HTTP): `nearest_within(primary, secondary, maxDistanceM)` for proximity joins, `point_in_geometry(lon, lat, geom)` for isochrone containment. The latter accepts a Polygon/MultiPolygon, a Feature, a GeometryCollection, or `{ geometry }` from the isochrone response.
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
## MCP server
|
|
348
|
+
|
|
349
|
+
Maplark has an MCP server to integrate OpenStreetMap data into AI and LLMs such as Claude, Cursor, and Copilot. However, it is implemented in another Python sister repo. See [maplark.com/products/mcp-server](https://maplark.com/products/mcp-server) for more details.
|
|
350
|
+
|
package/dist/geojson-feature.js
CHANGED
|
@@ -42,6 +42,15 @@ export function readTags(properties) {
|
|
|
42
42
|
}
|
|
43
43
|
return {};
|
|
44
44
|
}
|
|
45
|
+
function finiteLonLat(lon, lat) {
|
|
46
|
+
if (typeof lon !== 'number' || typeof lat !== 'number') {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (!Number.isFinite(lon) || !Number.isFinite(lat)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return [lon, lat];
|
|
53
|
+
}
|
|
45
54
|
function collectPositions(coordinates, out) {
|
|
46
55
|
if (!Array.isArray(coordinates) || coordinates.length === 0) {
|
|
47
56
|
return;
|
|
@@ -86,22 +95,20 @@ export function featureCentroid(feature) {
|
|
|
86
95
|
const group = geometryGroup(feature.geometry?.type?.trim() ?? '');
|
|
87
96
|
if (group === 'points') {
|
|
88
97
|
const coords = feature.geometry?.coordinates;
|
|
89
|
-
if (feature.geometry?.type === 'Point' && Array.isArray(coords)
|
|
90
|
-
return
|
|
98
|
+
if (feature.geometry?.type === 'Point' && Array.isArray(coords)) {
|
|
99
|
+
return finiteLonLat(coords[0], coords[1]);
|
|
91
100
|
}
|
|
92
101
|
if (feature.geometry?.type === 'MultiPoint' && Array.isArray(coords) && coords.length === 1) {
|
|
93
102
|
const point = coords[0];
|
|
94
|
-
|
|
95
|
-
return [point[0], point[1]];
|
|
96
|
-
}
|
|
103
|
+
return finiteLonLat(point?.[0], point?.[1]);
|
|
97
104
|
}
|
|
98
105
|
return null;
|
|
99
106
|
}
|
|
100
107
|
const centroid = feature.properties?.['centroid'];
|
|
101
108
|
if (centroid && typeof centroid === 'object' && !Array.isArray(centroid)) {
|
|
102
109
|
const coords = centroid.coordinates;
|
|
103
|
-
if (Array.isArray(coords)
|
|
104
|
-
return
|
|
110
|
+
if (Array.isArray(coords)) {
|
|
111
|
+
return finiteLonLat(coords[0], coords[1]);
|
|
105
112
|
}
|
|
106
113
|
}
|
|
107
114
|
return null;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Local geometry helpers. No HTTP. The planner must not invent containment. */
|
|
2
|
+
export type GeometryLike = {
|
|
3
|
+
type?: string;
|
|
4
|
+
coordinates?: unknown;
|
|
5
|
+
geometries?: unknown;
|
|
6
|
+
geometry?: GeometryLike | null;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Point-in-polygon for GeoJSON Polygon/MultiPolygon, including holes.
|
|
10
|
+
* Also accepts a Feature, GeometryCollection, or `{ geometry }` (isochrone body).
|
|
11
|
+
*/
|
|
12
|
+
export declare function point_in_geometry(lon: number, lat: number, geom: unknown): boolean;
|
package/dist/geometry.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Local geometry helpers. No HTTP. The planner must not invent containment. */
|
|
2
|
+
function pointInRing(lon, lat, ring) {
|
|
3
|
+
let inside = false;
|
|
4
|
+
const n = ring.length;
|
|
5
|
+
for (let i = 0; i < n; i += 1) {
|
|
6
|
+
const x1 = ring[i][0];
|
|
7
|
+
const y1 = ring[i][1];
|
|
8
|
+
const next = ring[(i + 1) % n];
|
|
9
|
+
const x2 = next[0];
|
|
10
|
+
const y2 = next[1];
|
|
11
|
+
if ((y1 > lat) !== (y2 > lat)) {
|
|
12
|
+
const xAtLat = x1 + ((lat - y1) * (x2 - x1)) / (y2 - y1);
|
|
13
|
+
if (lon < xAtLat) {
|
|
14
|
+
inside = !inside;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return inside;
|
|
19
|
+
}
|
|
20
|
+
function pointInPolygonRings(lon, lat, rings) {
|
|
21
|
+
let inside = false;
|
|
22
|
+
for (const ring of rings) {
|
|
23
|
+
if (Array.isArray(ring) && ring.length > 0 && pointInRing(lon, lat, ring)) {
|
|
24
|
+
inside = !inside;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return inside;
|
|
28
|
+
}
|
|
29
|
+
function pointInUnknown(lon, lat, geom, depth) {
|
|
30
|
+
if (geom == null || typeof geom !== 'object' || depth > 4) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const g = geom;
|
|
34
|
+
const gtype = g.type;
|
|
35
|
+
if (gtype === 'Polygon') {
|
|
36
|
+
const coords = g.coordinates;
|
|
37
|
+
return Array.isArray(coords) && pointInPolygonRings(lon, lat, coords);
|
|
38
|
+
}
|
|
39
|
+
if (gtype === 'MultiPolygon') {
|
|
40
|
+
const coords = g.coordinates;
|
|
41
|
+
return Array.isArray(coords)
|
|
42
|
+
&& coords.some((poly) => Array.isArray(poly) && poly.length > 0
|
|
43
|
+
&& pointInPolygonRings(lon, lat, poly));
|
|
44
|
+
}
|
|
45
|
+
if (gtype === 'GeometryCollection') {
|
|
46
|
+
const parts = g.geometries;
|
|
47
|
+
return Array.isArray(parts) && parts.some((part) => pointInUnknown(lon, lat, part, depth + 1));
|
|
48
|
+
}
|
|
49
|
+
if (gtype === 'Feature' || (gtype == null && g.geometry != null)) {
|
|
50
|
+
return pointInUnknown(lon, lat, g.geometry, depth + 1);
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Point-in-polygon for GeoJSON Polygon/MultiPolygon, including holes.
|
|
56
|
+
* Also accepts a Feature, GeometryCollection, or `{ geometry }` (isochrone body).
|
|
57
|
+
*/
|
|
58
|
+
export function point_in_geometry(lon, lat, geom) {
|
|
59
|
+
return pointInUnknown(lon, lat, geom, 0);
|
|
60
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -32,10 +32,13 @@ export type OSMFeaturesLayer = {
|
|
|
32
32
|
orTags?: string[];
|
|
33
33
|
notTags?: string[];
|
|
34
34
|
type?: string;
|
|
35
|
+
wayShape?: 'line' | 'polygon' | 'all';
|
|
36
|
+
/** @deprecated Use `wayShape`. */
|
|
35
37
|
shape?: 'line' | 'polygon' | 'all';
|
|
36
38
|
};
|
|
37
39
|
/** Flat query params (same idea as Python `query(**params)`). */
|
|
38
40
|
export type OSMFeaturesParams = OSMFeaturesLayer & {
|
|
41
|
+
/** Page size. Omit to use the API default (1000). Max `6000`. */
|
|
39
42
|
limit?: number;
|
|
40
43
|
cursor?: string;
|
|
41
44
|
zoom?: number;
|
|
@@ -49,8 +52,9 @@ export type OSMFeaturesParams = OSMFeaturesLayer & {
|
|
|
49
52
|
minAreaM2?: number;
|
|
50
53
|
maxAreaM2?: number;
|
|
51
54
|
disableBudgetWarning?: boolean;
|
|
55
|
+
/** When true, include `properties.centroid` on non-point features. Omit for the API default (false). */
|
|
52
56
|
centroid?: boolean;
|
|
53
|
-
/** When true
|
|
57
|
+
/** When true, clip returned geometry to the requested bbox. Omit for the API default (false). */
|
|
54
58
|
clipGeometry?: boolean;
|
|
55
59
|
/** Accept media type. Default application/geo+json; other types put bytes in ``data``. */
|
|
56
60
|
accept?: string;
|
|
@@ -93,6 +97,7 @@ export type PlacesSearchParams = {
|
|
|
93
97
|
tags?: string[];
|
|
94
98
|
orTags?: string[];
|
|
95
99
|
limit?: number;
|
|
100
|
+
/** Keep only places known open at ``asOf`` (or now). Untagged hours are dropped. */
|
|
96
101
|
openNow?: boolean;
|
|
97
102
|
asOf?: string;
|
|
98
103
|
};
|
|
@@ -103,9 +108,29 @@ export type PlacesNearbyParams = {
|
|
|
103
108
|
tags?: string[];
|
|
104
109
|
orTags?: string[];
|
|
105
110
|
limit?: number;
|
|
111
|
+
/** Keep only places known open at ``asOf`` (or now). Untagged hours are dropped. */
|
|
106
112
|
openNow?: boolean;
|
|
107
113
|
asOf?: string;
|
|
108
114
|
};
|
|
115
|
+
/**
|
|
116
|
+
* Place GeoJSON ``properties`` from search / nearby / details.
|
|
117
|
+
* ``openNow`` is true/false when hours are evaluable at ``asOf`` (or now);
|
|
118
|
+
* omitted when missing or unparseable.
|
|
119
|
+
*/
|
|
120
|
+
export type PlaceProperties = {
|
|
121
|
+
tags?: Record<string, unknown>;
|
|
122
|
+
openNow?: boolean;
|
|
123
|
+
centroid?: unknown;
|
|
124
|
+
};
|
|
125
|
+
export type PlaceFeatureLike = {
|
|
126
|
+
properties?: PlaceProperties | Record<string, unknown>;
|
|
127
|
+
/** ``places_details`` envelope: ``{ status, feature }``. */
|
|
128
|
+
feature?: PlaceFeatureLike;
|
|
129
|
+
};
|
|
130
|
+
/** ``properties.openNow === true`` (known open). Missing / non-boolean is not open. Unwraps ``{ feature }``. */
|
|
131
|
+
export declare function isOpenNow(feature: PlaceFeatureLike | undefined): boolean;
|
|
132
|
+
/** ``true`` / ``false`` when hours are known; ``undefined`` when the field is omitted. Unwraps ``{ feature }``. */
|
|
133
|
+
export declare function readOpenNow(feature: PlaceFeatureLike | undefined): boolean | undefined;
|
|
109
134
|
export type PlacesDetailsParams = {
|
|
110
135
|
/** ``node`` / ``way`` / ``relation``, or a full ``node/123`` feature id. */
|
|
111
136
|
osmType: string;
|
|
@@ -146,10 +171,10 @@ export declare class OSMFeatures {
|
|
|
146
171
|
/** Single HTTP request with retry. Throws on non-OK (same role as Python `_raw_query`). */
|
|
147
172
|
private _rawQuery;
|
|
148
173
|
/** Single upstream page. Params map 1:1 to server query string (no tiling). */
|
|
149
|
-
query({ bbox, tags, orTags, notTags, type, shape, limit, cursor, zoom, location, radius, osmIds, minLengthM, maxLengthM, minAreaM2, maxAreaM2, disableBudgetWarning, centroid, clipGeometry, accept, }: OSMFeaturesParams, dependencies?: OSMFeaturesDependencies): Promise<OSMFeaturesResult>;
|
|
174
|
+
query({ bbox, tags, orTags, notTags, type, wayShape, shape, limit, cursor, zoom, location, radius, osmIds, minLengthM, maxLengthM, minAreaM2, maxAreaM2, disableBudgetWarning, centroid, clipGeometry, accept, }: OSMFeaturesParams, dependencies?: OSMFeaturesDependencies): Promise<OSMFeaturesResult>;
|
|
150
175
|
/** Auto-paginate (and optionally tile) until complete. Each page uses `_rawQuery`. */
|
|
151
|
-
query_all({ bbox, tags, orTags, notTags, type, shape, zoom, location, radius, osmIds, minLengthM, maxLengthM, minAreaM2, maxAreaM2, disableBudgetWarning, centroid, clipGeometry, accept, limitPerPage, bboxTiles, maxPages, maxFeatures, }: Omit<OSMFeaturesParams, 'limit' | 'cursor'> & {
|
|
152
|
-
/** Upstream `limit` per HTTP request (page size). */
|
|
176
|
+
query_all({ bbox, tags, orTags, notTags, type, wayShape, shape, zoom, location, radius, osmIds, minLengthM, maxLengthM, minAreaM2, maxAreaM2, disableBudgetWarning, centroid, clipGeometry, accept, limitPerPage, bboxTiles, maxPages, maxFeatures, }: Omit<OSMFeaturesParams, 'limit' | 'cursor'> & {
|
|
177
|
+
/** Upstream `limit` per HTTP request (page size). Omit to use the API default (1000). */
|
|
153
178
|
limitPerPage?: number;
|
|
154
179
|
bboxTiles?: number;
|
|
155
180
|
maxPages?: number;
|
|
@@ -160,11 +185,11 @@ export declare class OSMFeatures {
|
|
|
160
185
|
estimate_cost(params?: OSMFeaturesParams, dependencies?: OSMFeaturesDependencies): Promise<Record<string, unknown>>;
|
|
161
186
|
/** This month's unit-budget usage (``GET /v1/usage``). */
|
|
162
187
|
usage(dependencies?: OSMFeaturesDependencies): Promise<Record<string, unknown>>;
|
|
163
|
-
/** Find places in a bbox or radius (``POST /v1/places/search``). */
|
|
188
|
+
/** Find places in a bbox or radius (``POST /v1/places/search``). Features have ``properties.openNow`` when hours are known. */
|
|
164
189
|
places_search(params: PlacesSearchParams, dependencies?: OSMFeaturesDependencies): Promise<Record<string, unknown>>;
|
|
165
|
-
/** Nearest places from a point (``POST /v1/places/nearby``). */
|
|
190
|
+
/** Nearest places from a point (``POST /v1/places/nearby``). Features have ``properties.openNow`` when hours are known. */
|
|
166
191
|
places_nearby(params: PlacesNearbyParams, dependencies?: OSMFeaturesDependencies): Promise<Record<string, unknown>>;
|
|
167
|
-
/** One place by OSM id (``GET /v1/places/{osm_type}/{osm_id}``). */
|
|
192
|
+
/** One place by OSM id (``GET /v1/places/{osm_type}/{osm_id}``). Feature has ``properties.openNow`` when hours are known. */
|
|
168
193
|
places_details(params: PlacesDetailsParams, dependencies?: OSMFeaturesDependencies): Promise<Record<string, unknown>>;
|
|
169
194
|
/** Reach polygon along the walk/bike network (``POST /v1/routes/isochrone``). */
|
|
170
195
|
routes_isochrone(params: RoutesIsochroneParams, dependencies?: OSMFeaturesDependencies): Promise<Record<string, unknown>>;
|
|
@@ -174,4 +199,6 @@ export declare class OSMFeatures {
|
|
|
174
199
|
routes_optimized_path(params: RoutesOptimizedPathParams, dependencies?: OSMFeaturesDependencies): Promise<Record<string, unknown>>;
|
|
175
200
|
}
|
|
176
201
|
export * from './geojson-feature.js';
|
|
202
|
+
export * from './geometry.js';
|
|
203
|
+
export * from './nearest.js';
|
|
177
204
|
export * from './preset/index.js';
|
package/dist/index.js
CHANGED
|
@@ -21,9 +21,30 @@ function metaFromHeaders(headers, featureCount) {
|
|
|
21
21
|
}
|
|
22
22
|
return meta;
|
|
23
23
|
}
|
|
24
|
+
function openNowValue(feature) {
|
|
25
|
+
if (feature == null) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const props = feature.properties;
|
|
29
|
+
if (props && typeof props === 'object' && 'openNow' in props) {
|
|
30
|
+
return props['openNow'];
|
|
31
|
+
}
|
|
32
|
+
if (feature.feature != null && feature.feature !== feature) {
|
|
33
|
+
return openNowValue(feature.feature);
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
/** ``properties.openNow === true`` (known open). Missing / non-boolean is not open. Unwraps ``{ feature }``. */
|
|
38
|
+
export function isOpenNow(feature) {
|
|
39
|
+
return openNowValue(feature) === true;
|
|
40
|
+
}
|
|
41
|
+
/** ``true`` / ``false`` when hours are known; ``undefined`` when the field is omitted. Unwraps ``{ feature }``. */
|
|
42
|
+
export function readOpenNow(feature) {
|
|
43
|
+
const value = openNowValue(feature);
|
|
44
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
45
|
+
}
|
|
24
46
|
const PLACE_TYPES = new Set(['node', 'way', 'relation']);
|
|
25
47
|
const DEFAULT_BASE_URL = 'https://api.maplark.com';
|
|
26
|
-
const DEFAULT_LIMIT = 1000;
|
|
27
48
|
const MAX_LIMIT = 6000;
|
|
28
49
|
const GEOJSON_ACCEPT = 'application/geo+json';
|
|
29
50
|
function isGeojsonAccept(accept) {
|
|
@@ -88,14 +109,14 @@ function optionalBoolean(query, key) {
|
|
|
88
109
|
}
|
|
89
110
|
return undefined;
|
|
90
111
|
}
|
|
91
|
-
function parseLimit(query
|
|
112
|
+
function parseLimit(query) {
|
|
92
113
|
const raw = optionalString(query, 'limit');
|
|
93
114
|
if (raw == null) {
|
|
94
|
-
return
|
|
115
|
+
return undefined;
|
|
95
116
|
}
|
|
96
117
|
const parsed = Number.parseInt(raw, 10);
|
|
97
118
|
if (!Number.isFinite(parsed)) {
|
|
98
|
-
return
|
|
119
|
+
return undefined;
|
|
99
120
|
}
|
|
100
121
|
if (parsed > MAX_LIMIT) {
|
|
101
122
|
throw appError(400, 'invalid_limit', `limit must be <= ${MAX_LIMIT}.`);
|
|
@@ -149,7 +170,9 @@ function buildFeaturesQuery(params) {
|
|
|
149
170
|
if (params.bbox) {
|
|
150
171
|
query.set('bbox', params.bbox);
|
|
151
172
|
}
|
|
152
|
-
|
|
173
|
+
if (params.limit != null) {
|
|
174
|
+
query.set('limit', String(params.limit));
|
|
175
|
+
}
|
|
153
176
|
if (params.cursor) {
|
|
154
177
|
query.set('cursor', params.cursor);
|
|
155
178
|
}
|
|
@@ -177,10 +200,10 @@ function buildFeaturesQuery(params) {
|
|
|
177
200
|
if (params.maxAreaM2 != null) {
|
|
178
201
|
query.set('max_area_m2', String(params.maxAreaM2));
|
|
179
202
|
}
|
|
180
|
-
if (params.disableBudgetWarning
|
|
203
|
+
if (params.disableBudgetWarning) {
|
|
181
204
|
query.set('disable_budget_warning', String(params.disableBudgetWarning));
|
|
182
205
|
}
|
|
183
|
-
if (params.centroid
|
|
206
|
+
if (params.centroid) {
|
|
184
207
|
query.set('centroid', String(params.centroid));
|
|
185
208
|
}
|
|
186
209
|
if (params.clipGeometry != null) {
|
|
@@ -189,8 +212,9 @@ function buildFeaturesQuery(params) {
|
|
|
189
212
|
if (params.type) {
|
|
190
213
|
query.set('type', params.type);
|
|
191
214
|
}
|
|
192
|
-
|
|
193
|
-
|
|
215
|
+
const wayShape = params.wayShape ?? params.shape;
|
|
216
|
+
if (wayShape) {
|
|
217
|
+
query.set('way_shape', wayShape);
|
|
194
218
|
}
|
|
195
219
|
for (const tag of params.tags ?? []) {
|
|
196
220
|
query.append('tags', tag);
|
|
@@ -231,7 +255,7 @@ function parsePlaceRef(osmType, osmId) {
|
|
|
231
255
|
return { osmType: kind, osmId: n };
|
|
232
256
|
}
|
|
233
257
|
function placesSearchBody(params) {
|
|
234
|
-
const body = {
|
|
258
|
+
const body = {};
|
|
235
259
|
if (params.bbox != null) {
|
|
236
260
|
body['bbox'] = params.bbox;
|
|
237
261
|
}
|
|
@@ -250,6 +274,9 @@ function placesSearchBody(params) {
|
|
|
250
274
|
if (params.orTags?.length) {
|
|
251
275
|
body['orTags'] = params.orTags;
|
|
252
276
|
}
|
|
277
|
+
if (params.limit != null) {
|
|
278
|
+
body['limit'] = params.limit;
|
|
279
|
+
}
|
|
253
280
|
if (params.openNow) {
|
|
254
281
|
body['openNow'] = true;
|
|
255
282
|
}
|
|
@@ -261,9 +288,10 @@ function placesSearchBody(params) {
|
|
|
261
288
|
function placesNearbyBody(params) {
|
|
262
289
|
const body = {
|
|
263
290
|
location: latlng(params.location),
|
|
264
|
-
radius: params.radius ?? 1000,
|
|
265
|
-
limit: params.limit ?? 10,
|
|
266
291
|
};
|
|
292
|
+
if (params.radius != null) {
|
|
293
|
+
body['radius'] = params.radius;
|
|
294
|
+
}
|
|
267
295
|
if (params.type != null) {
|
|
268
296
|
body['type'] = params.type;
|
|
269
297
|
}
|
|
@@ -273,6 +301,9 @@ function placesNearbyBody(params) {
|
|
|
273
301
|
if (params.orTags?.length) {
|
|
274
302
|
body['orTags'] = params.orTags;
|
|
275
303
|
}
|
|
304
|
+
if (params.limit != null) {
|
|
305
|
+
body['limit'] = params.limit;
|
|
306
|
+
}
|
|
276
307
|
if (params.openNow) {
|
|
277
308
|
body['openNow'] = true;
|
|
278
309
|
}
|
|
@@ -284,7 +315,6 @@ function placesNearbyBody(params) {
|
|
|
284
315
|
function routesIsochroneBody(params) {
|
|
285
316
|
const body = {
|
|
286
317
|
origin: lonlat(params.origin),
|
|
287
|
-
travelMode: params.travelMode ?? 'WALK',
|
|
288
318
|
};
|
|
289
319
|
if (params.maxDistanceM != null) {
|
|
290
320
|
body['max_distance_m'] = params.maxDistanceM;
|
|
@@ -295,28 +325,37 @@ function routesIsochroneBody(params) {
|
|
|
295
325
|
if (params.searchBufferM != null) {
|
|
296
326
|
body['search_buffer_m'] = params.searchBufferM;
|
|
297
327
|
}
|
|
328
|
+
if (params.travelMode != null) {
|
|
329
|
+
body['travelMode'] = params.travelMode;
|
|
330
|
+
}
|
|
298
331
|
return body;
|
|
299
332
|
}
|
|
300
333
|
function routesPathBody(params) {
|
|
301
334
|
const body = {
|
|
302
335
|
stops: params.stops.map(lonlat),
|
|
303
|
-
travelMode: params.travelMode ?? 'WALK',
|
|
304
336
|
};
|
|
305
337
|
if (params.searchBufferM != null) {
|
|
306
338
|
body['search_buffer_m'] = params.searchBufferM;
|
|
307
339
|
}
|
|
340
|
+
if (params.travelMode != null) {
|
|
341
|
+
body['travelMode'] = params.travelMode;
|
|
342
|
+
}
|
|
308
343
|
return body;
|
|
309
344
|
}
|
|
310
345
|
function routesOptimizedPathBody(params) {
|
|
311
346
|
const body = {
|
|
312
347
|
start: lonlat(params.start),
|
|
313
348
|
stops: params.stops.map(lonlat),
|
|
314
|
-
loop: params.loop ?? true,
|
|
315
|
-
travelMode: params.travelMode ?? 'WALK',
|
|
316
349
|
};
|
|
350
|
+
if (params.loop != null) {
|
|
351
|
+
body['loop'] = params.loop;
|
|
352
|
+
}
|
|
317
353
|
if (params.searchBufferM != null) {
|
|
318
354
|
body['search_buffer_m'] = params.searchBufferM;
|
|
319
355
|
}
|
|
356
|
+
if (params.travelMode != null) {
|
|
357
|
+
body['travelMode'] = params.travelMode;
|
|
358
|
+
}
|
|
320
359
|
return body;
|
|
321
360
|
}
|
|
322
361
|
function sleep(ms) {
|
|
@@ -518,13 +557,14 @@ export class OSMFeatures {
|
|
|
518
557
|
};
|
|
519
558
|
}
|
|
520
559
|
/** Single upstream page. Params map 1:1 to server query string (no tiling). */
|
|
521
|
-
async query({ bbox, tags, orTags, notTags, type, shape, limit
|
|
560
|
+
async query({ bbox, tags, orTags, notTags, type, wayShape, shape, limit, cursor, zoom, location, radius, osmIds, minLengthM, maxLengthM, minAreaM2, maxAreaM2, disableBudgetWarning, centroid, clipGeometry, accept, }, dependencies = {}) {
|
|
522
561
|
const payload = await this._rawQuery({
|
|
523
562
|
bbox,
|
|
524
563
|
tags,
|
|
525
564
|
orTags,
|
|
526
565
|
notTags,
|
|
527
566
|
type,
|
|
567
|
+
wayShape,
|
|
528
568
|
shape,
|
|
529
569
|
limit,
|
|
530
570
|
cursor,
|
|
@@ -544,7 +584,7 @@ export class OSMFeatures {
|
|
|
544
584
|
return payload;
|
|
545
585
|
}
|
|
546
586
|
/** Auto-paginate (and optionally tile) until complete. Each page uses `_rawQuery`. */
|
|
547
|
-
async query_all({ bbox, tags, orTags, notTags, type, shape, zoom, location, radius, osmIds, minLengthM, maxLengthM, minAreaM2, maxAreaM2, disableBudgetWarning, centroid, clipGeometry, accept, limitPerPage
|
|
587
|
+
async query_all({ bbox, tags, orTags, notTags, type, wayShape, shape, zoom, location, radius, osmIds, minLengthM, maxLengthM, minAreaM2, maxAreaM2, disableBudgetWarning, centroid, clipGeometry, accept, limitPerPage, bboxTiles = 2, maxPages = 15, maxFeatures = 55_000, }, dependencies = {}) {
|
|
548
588
|
if (!isGeojsonAccept(accept)) {
|
|
549
589
|
throw appError(400, 'invalid_accept', 'query_all only supports GeoJSON; use query({ accept }) for binary encodings.');
|
|
550
590
|
}
|
|
@@ -570,6 +610,7 @@ export class OSMFeatures {
|
|
|
570
610
|
orTags,
|
|
571
611
|
notTags,
|
|
572
612
|
type,
|
|
613
|
+
wayShape,
|
|
573
614
|
shape,
|
|
574
615
|
limit: limitPerPage,
|
|
575
616
|
zoom,
|
|
@@ -670,8 +711,8 @@ export class OSMFeatures {
|
|
|
670
711
|
orTags: params.orTags,
|
|
671
712
|
notTags: params.notTags,
|
|
672
713
|
type: params.type,
|
|
673
|
-
|
|
674
|
-
limit: params.limit
|
|
714
|
+
wayShape: params.wayShape ?? params.shape,
|
|
715
|
+
limit: params.limit,
|
|
675
716
|
zoom: params.zoom,
|
|
676
717
|
location: params.location,
|
|
677
718
|
radius: params.radius,
|
|
@@ -689,15 +730,15 @@ export class OSMFeatures {
|
|
|
689
730
|
async usage(dependencies = {}) {
|
|
690
731
|
return this._getJson('/v1/usage', {}, dependencies.fetchFn ?? fetch, dependencies.sleepFn ?? sleep, dependencies.nowFn ?? Date.now);
|
|
691
732
|
}
|
|
692
|
-
/** Find places in a bbox or radius (``POST /v1/places/search``). */
|
|
733
|
+
/** Find places in a bbox or radius (``POST /v1/places/search``). Features have ``properties.openNow`` when hours are known. */
|
|
693
734
|
async places_search(params, dependencies = {}) {
|
|
694
735
|
return this._postJson('/v1/places/search', placesSearchBody(params), dependencies.fetchFn ?? fetch, dependencies.sleepFn ?? sleep, dependencies.nowFn ?? Date.now);
|
|
695
736
|
}
|
|
696
|
-
/** Nearest places from a point (``POST /v1/places/nearby``). */
|
|
737
|
+
/** Nearest places from a point (``POST /v1/places/nearby``). Features have ``properties.openNow`` when hours are known. */
|
|
697
738
|
async places_nearby(params, dependencies = {}) {
|
|
698
739
|
return this._postJson('/v1/places/nearby', placesNearbyBody(params), dependencies.fetchFn ?? fetch, dependencies.sleepFn ?? sleep, dependencies.nowFn ?? Date.now);
|
|
699
740
|
}
|
|
700
|
-
/** One place by OSM id (``GET /v1/places/{osm_type}/{osm_id}``). */
|
|
741
|
+
/** One place by OSM id (``GET /v1/places/{osm_type}/{osm_id}``). Feature has ``properties.openNow`` when hours are known. */
|
|
701
742
|
async places_details(params, dependencies = {}) {
|
|
702
743
|
const { osmType, osmId } = parsePlaceRef(params.osmType, params.osmId);
|
|
703
744
|
return this._getJson(`/v1/places/${osmType}/${osmId}`, {}, dependencies.fetchFn ?? fetch, dependencies.sleepFn ?? sleep, dependencies.nowFn ?? Date.now);
|
|
@@ -716,4 +757,6 @@ export class OSMFeatures {
|
|
|
716
757
|
}
|
|
717
758
|
}
|
|
718
759
|
export * from './geojson-feature.js';
|
|
760
|
+
export * from './geometry.js';
|
|
761
|
+
export * from './nearest.js';
|
|
719
762
|
export * from './preset/index.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Local nearest-neighbor join over two GeoJSON feature sets. No HTTP. */
|
|
2
|
+
export declare const MAX_COMPARISONS = 500000;
|
|
3
|
+
export type NearestWithinPair = {
|
|
4
|
+
feature: unknown;
|
|
5
|
+
distance_m: number;
|
|
6
|
+
nearest: unknown;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Nearest secondary for each primary, within `maxDistanceM`.
|
|
10
|
+
*
|
|
11
|
+
* Accepts a FeatureCollection, a `{ data, meta }` `query` / `query_all` result,
|
|
12
|
+
* or a list of Features. Point comes from `geometry` when it is a Point, else
|
|
13
|
+
* `properties.centroid` (same rules as `featureCentroid`). A feature with
|
|
14
|
+
* neither throws.
|
|
15
|
+
*
|
|
16
|
+
* Returns pairs sorted by `distance_m`:
|
|
17
|
+
* `{ feature, distance_m, nearest }`
|
|
18
|
+
*
|
|
19
|
+
* `limit` keeps the closest pairs (SDK default 20). Pass `null` for every
|
|
20
|
+
* primary that has a match. O(n×m) haversine. Fine at search `limit`
|
|
21
|
+
* (default 100). Joins whose `len(primary) * len(secondary)` exceeds
|
|
22
|
+
* `maxComparisons` (default 500_000) throw. Pass `maxComparisons: null`
|
|
23
|
+
* for no cap.
|
|
24
|
+
*/
|
|
25
|
+
export declare function nearest_within(primary: unknown, secondary: unknown, maxDistanceM: number, { limit, maxComparisons, }?: {
|
|
26
|
+
limit?: number | null;
|
|
27
|
+
maxComparisons?: number | null;
|
|
28
|
+
}): NearestWithinPair[];
|
package/dist/nearest.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** Local nearest-neighbor join over two GeoJSON feature sets. No HTTP. */
|
|
2
|
+
import { featureCentroid } from './geojson-feature.js';
|
|
3
|
+
// Mean Earth radius. ponytail: sphere, not WGS84 ellipsoid; swap if you need centimetre-grade distances.
|
|
4
|
+
const EARTH_RADIUS_M = 6_371_000;
|
|
5
|
+
// O(n×m) haversine. 500k is ~1000×500 or 10_000×50; search-sized joins fit.
|
|
6
|
+
export const MAX_COMPARISONS = 500_000;
|
|
7
|
+
function haversineM(lon1, lat1, lon2, lat2) {
|
|
8
|
+
const p1 = (lat1 * Math.PI) / 180;
|
|
9
|
+
const p2 = (lat2 * Math.PI) / 180;
|
|
10
|
+
const dlat = ((lat2 - lat1) * Math.PI) / 180;
|
|
11
|
+
const dlon = ((lon2 - lon1) * Math.PI) / 180;
|
|
12
|
+
const a = Math.sin(dlat / 2) ** 2 + Math.cos(p1) * Math.cos(p2) * Math.sin(dlon / 2) ** 2;
|
|
13
|
+
return 2 * EARTH_RADIUS_M * Math.asin(Math.sqrt(Math.min(a, 1)));
|
|
14
|
+
}
|
|
15
|
+
function features(src) {
|
|
16
|
+
if (Array.isArray(src)) {
|
|
17
|
+
return src;
|
|
18
|
+
}
|
|
19
|
+
if (src && typeof src === 'object') {
|
|
20
|
+
const record = src;
|
|
21
|
+
if (Array.isArray(record['features'])) {
|
|
22
|
+
return record['features'];
|
|
23
|
+
}
|
|
24
|
+
const data = record['data'];
|
|
25
|
+
if (data && typeof data === 'object' && Array.isArray(data.features)) {
|
|
26
|
+
return data.features;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
throw new TypeError('expected a FeatureCollection or list of Features');
|
|
30
|
+
}
|
|
31
|
+
function lonLat(feat) {
|
|
32
|
+
if (!feat || typeof feat !== 'object') {
|
|
33
|
+
throw new Error('feature must be a GeoJSON Feature dict');
|
|
34
|
+
}
|
|
35
|
+
const record = feat;
|
|
36
|
+
const point = featureCentroid({
|
|
37
|
+
geometry: (record['geometry'] ?? null),
|
|
38
|
+
properties: record['properties'],
|
|
39
|
+
});
|
|
40
|
+
if (point == null || !Number.isFinite(point[0]) || !Number.isFinite(point[1])) {
|
|
41
|
+
throw new Error('feature has no Point geometry or properties.centroid');
|
|
42
|
+
}
|
|
43
|
+
return point;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Nearest secondary for each primary, within `maxDistanceM`.
|
|
47
|
+
*
|
|
48
|
+
* Accepts a FeatureCollection, a `{ data, meta }` `query` / `query_all` result,
|
|
49
|
+
* or a list of Features. Point comes from `geometry` when it is a Point, else
|
|
50
|
+
* `properties.centroid` (same rules as `featureCentroid`). A feature with
|
|
51
|
+
* neither throws.
|
|
52
|
+
*
|
|
53
|
+
* Returns pairs sorted by `distance_m`:
|
|
54
|
+
* `{ feature, distance_m, nearest }`
|
|
55
|
+
*
|
|
56
|
+
* `limit` keeps the closest pairs (SDK default 20). Pass `null` for every
|
|
57
|
+
* primary that has a match. O(n×m) haversine. Fine at search `limit`
|
|
58
|
+
* (default 100). Joins whose `len(primary) * len(secondary)` exceeds
|
|
59
|
+
* `maxComparisons` (default 500_000) throw. Pass `maxComparisons: null`
|
|
60
|
+
* for no cap.
|
|
61
|
+
*/
|
|
62
|
+
export function nearest_within(primary, secondary, maxDistanceM, { limit = 20, maxComparisons = MAX_COMPARISONS, } = {}) {
|
|
63
|
+
if (limit != null && limit < 1) {
|
|
64
|
+
throw new Error('limit must be a positive int');
|
|
65
|
+
}
|
|
66
|
+
if (maxComparisons != null && maxComparisons < 1) {
|
|
67
|
+
throw new Error('max_comparisons must be a positive int');
|
|
68
|
+
}
|
|
69
|
+
const primaries = features(primary);
|
|
70
|
+
const secondaries = features(secondary);
|
|
71
|
+
if (primaries.length === 0 || secondaries.length === 0) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
const n = primaries.length;
|
|
75
|
+
const m = secondaries.length;
|
|
76
|
+
if (maxComparisons != null && n * m > maxComparisons) {
|
|
77
|
+
throw new Error(`nearest_within join is ${n}×${m} comparisons `
|
|
78
|
+
+ `(cap ${maxComparisons}). Shrink the collections `
|
|
79
|
+
+ '(places_search/nearby limit, not query_all).');
|
|
80
|
+
}
|
|
81
|
+
const secPts = secondaries.map((s) => [s, lonLat(s)]);
|
|
82
|
+
const pairs = [];
|
|
83
|
+
for (const p of primaries) {
|
|
84
|
+
const [plon, plat] = lonLat(p);
|
|
85
|
+
let best = null;
|
|
86
|
+
for (const [s, [slon, slat]] of secPts) {
|
|
87
|
+
const d = haversineM(plon, plat, slon, slat);
|
|
88
|
+
if (best === null || d < best[0]) {
|
|
89
|
+
best = [d, s];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (best !== null && best[0] <= maxDistanceM) {
|
|
93
|
+
pairs.push({ feature: p, distance_m: best[0], nearest: best[1] });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
pairs.sort((a, b) => a.distance_m - b.distance_m);
|
|
97
|
+
return limit == null ? pairs : pairs.slice(0, limit);
|
|
98
|
+
}
|
package/dist/preset/catalog.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export type OSMFeaturesLayerPreset = {
|
|
|
7
7
|
orTags?: string[];
|
|
8
8
|
notTags?: string[];
|
|
9
9
|
type?: string;
|
|
10
|
+
wayShape?: 'line' | 'polygon';
|
|
11
|
+
/** @deprecated Use `wayShape`. */
|
|
10
12
|
shape?: 'line' | 'polygon';
|
|
11
13
|
};
|
|
12
14
|
export declare const OSM_FEATURES_LAYER_PRESETS: Record<OSMFeaturesPresetId, OSMFeaturesLayerPreset>;
|
package/dist/preset/catalog.js
CHANGED
|
@@ -4,14 +4,14 @@ export const OSM_FEATURES_LAYER_PRESETS = {
|
|
|
4
4
|
label: 'Buildings',
|
|
5
5
|
tags: ['building'],
|
|
6
6
|
type: 'way,relation',
|
|
7
|
-
|
|
7
|
+
wayShape: 'polygon',
|
|
8
8
|
},
|
|
9
9
|
roads_paths: {
|
|
10
10
|
id: 'roads_paths',
|
|
11
11
|
label: 'Roads & paths',
|
|
12
12
|
tags: ['highway'],
|
|
13
13
|
type: 'way,relation',
|
|
14
|
-
|
|
14
|
+
wayShape: 'line',
|
|
15
15
|
},
|
|
16
16
|
parks_green_space: {
|
|
17
17
|
id: 'parks_green_space',
|
|
@@ -37,7 +37,7 @@ export const OSM_FEATURES_LAYER_PRESETS = {
|
|
|
37
37
|
'amenity=pub',
|
|
38
38
|
'amenity=biergarten',
|
|
39
39
|
],
|
|
40
|
-
|
|
40
|
+
wayShape: 'polygon',
|
|
41
41
|
},
|
|
42
42
|
shops_commerce: {
|
|
43
43
|
id: 'shops_commerce',
|
|
@@ -112,7 +112,7 @@ export const OSM_FEATURES_LAYER_PRESETS = {
|
|
|
112
112
|
'waterway=ditch',
|
|
113
113
|
],
|
|
114
114
|
type: 'way,relation',
|
|
115
|
-
|
|
115
|
+
wayShape: 'line',
|
|
116
116
|
},
|
|
117
117
|
};
|
|
118
118
|
export const OSM_FEATURES_LAYER_PRESET_ORDER = [
|
package/dist/preset/resolve.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export type ResolvedCustomLayer = {
|
|
|
12
12
|
orTags?: string[];
|
|
13
13
|
notTags?: string[];
|
|
14
14
|
type?: string;
|
|
15
|
+
wayShape?: 'line' | 'polygon' | 'all';
|
|
16
|
+
/** @deprecated Use `wayShape`. */
|
|
15
17
|
shape?: 'line' | 'polygon' | 'all';
|
|
16
18
|
};
|
|
17
19
|
export type ResolvedDemoLayer = ResolvedPresetLayer | ResolvedCustomLayer;
|
|
@@ -24,7 +26,7 @@ export declare function getPreset(id: string): OSMFeaturesLayerPreset | undefine
|
|
|
24
26
|
export declare function resolvePresetFromQuery(query: PresetQuery): ResolvedPresetLayer;
|
|
25
27
|
/**
|
|
26
28
|
* Custom layer: `tags` / `or_tags` / `not_tags` + required `bbox`.
|
|
27
|
-
* Optional `type` / `
|
|
29
|
+
* Optional `type` / `way_shape`. Needs at least one positive filter (`tags` or `or_tags`).
|
|
28
30
|
*/
|
|
29
31
|
export declare function resolveCustomLayerFromQuery(query: PresetQuery): ResolvedCustomLayer;
|
|
30
32
|
/**
|
package/dist/preset/resolve.js
CHANGED
|
@@ -91,7 +91,7 @@ export function resolvePresetFromQuery(query) {
|
|
|
91
91
|
}
|
|
92
92
|
/**
|
|
93
93
|
* Custom layer: `tags` / `or_tags` / `not_tags` + required `bbox`.
|
|
94
|
-
* Optional `type` / `
|
|
94
|
+
* Optional `type` / `way_shape`. Needs at least one positive filter (`tags` or `or_tags`).
|
|
95
95
|
*/
|
|
96
96
|
export function resolveCustomLayerFromQuery(query) {
|
|
97
97
|
const tags = stringList(query, 'tags');
|
|
@@ -114,9 +114,9 @@ export function resolveCustomLayerFromQuery(query) {
|
|
|
114
114
|
if (type) {
|
|
115
115
|
layer.type = type;
|
|
116
116
|
}
|
|
117
|
-
const
|
|
118
|
-
if (
|
|
119
|
-
layer.
|
|
117
|
+
const wayShape = optionalString(query, 'way_shape') ?? optionalString(query, 'shape');
|
|
118
|
+
if (wayShape === 'line' || wayShape === 'polygon' || wayShape === 'all') {
|
|
119
|
+
layer.wayShape = wayShape;
|
|
120
120
|
}
|
|
121
121
|
return layer;
|
|
122
122
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "osmfeatures",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Get OpenStreetMap features such as buildings, roads, or points of interest from dedicated servers with a single API call in GeoJSON, FlatGeobuf, Geoparquet, and CSV formats.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "tsc -p tsconfig.json",
|
|
21
|
-
"test": "tsx src/index.test.ts && tsx src/geojson-feature.test.ts && tsx src/preset/preset.test.ts",
|
|
21
|
+
"test": "tsx src/index.test.ts && tsx src/geojson-feature.test.ts && tsx src/geometry.test.ts && tsx src/nearest.test.ts && tsx src/preset/preset.test.ts && tsx src/tutorial.test.ts",
|
|
22
22
|
"prepare": "npm run build",
|
|
23
23
|
"prepublishOnly": "npm run build"
|
|
24
24
|
},
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"homepage": "https://maplark.com",
|
|
37
37
|
"repository": {
|
|
38
38
|
"type": "git",
|
|
39
|
-
"url": "https://github.com/MapLark/osmfeatures-
|
|
39
|
+
"url": "https://github.com/MapLark/osmfeatures-ts.git"
|
|
40
40
|
},
|
|
41
41
|
"license": "MIT",
|
|
42
42
|
"engines": {
|