osmfeatures 0.2.4 → 0.5.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 +124 -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 +70 -7
- package/dist/index.js +184 -22
- package/dist/nearest.d.ts +28 -0
- package/dist/nearest.js +98 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,28 +1,42 @@
|
|
|
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
|
+
- [stats](#stats)
|
|
14
|
+
- [estimate_cost](#estimate_cost)
|
|
15
|
+
- [usage](#usage)
|
|
16
|
+
- [Places and routes](#places-and-routes)
|
|
17
|
+
- [Places search](#places-search)
|
|
18
|
+
- [Nearby](#nearby-ranked-from-a-point)
|
|
19
|
+
- [Place details](#place-details)
|
|
20
|
+
- [Opening hours](#opening-hours)
|
|
21
|
+
- [X near Y](#x-near-y-local-join)
|
|
22
|
+
- [Walk and bike routes](#walk-and-bike-routes)
|
|
23
|
+
- [MCP server](#mcp-server)
|
|
12
24
|
|
|
13
|
-
|
|
25
|
+
OSM types map to GeoJSON the way GIS tools expect:
|
|
14
26
|
|
|
15
|
-
|
|
27
|
+
- `node` → Point
|
|
28
|
+
- `way` → LineString or Polygon
|
|
29
|
+
- `relation` → MultiPolygon or a bundle of geometries
|
|
16
30
|
|
|
17
|
-
|
|
18
|
-
- `way_shape=polygon` - closed ways (buildings, parks) or multipolygon relations.
|
|
19
|
-
- `way_shape=all` - both shapes (default when way_shape is omitted).
|
|
31
|
+
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
32
|
|
|
21
|
-
|
|
33
|
+
Use `way_shape` when you need lines vs areas:
|
|
22
34
|
|
|
23
|
-
`
|
|
35
|
+
- `way_shape=line` — unclosed ways (streets, footpaths, rivers) and line-like relations (routes, some boundaries)
|
|
36
|
+
- `way_shape=polygon` — closed ways (building footprints, parks) and multipolygon relations
|
|
37
|
+
- `way_shape=all` — both (the default if you leave it off)
|
|
24
38
|
|
|
25
|
-
|
|
39
|
+
Buildings in a box: `type=way & tags=building` — the same idea as Overpass `way[building]`.
|
|
26
40
|
|
|
27
41
|
# Quick start
|
|
28
42
|
|
|
@@ -75,6 +89,7 @@ The geographical area for the request in terms of GPS coordinates or specific OS
|
|
|
75
89
|
| `bbox` | `string` | Bounding box as `min_lon,min_lat,max_lon,max_lat`. |
|
|
76
90
|
| `location` | `string` | Point for a radius search as `lat,lng`. Requires `radius`. |
|
|
77
91
|
| `radius` | `number` | Search radius in metres. Requires `location`. |
|
|
92
|
+
| `within` | `string` | Polygon spatial anchor as `way/<id>` or `relation/<id>`. Mutually exclusive with `bbox` / `location`. |
|
|
78
93
|
| `osmIds` | `string` | Comma-separated OSM IDs to fetch by id. |
|
|
79
94
|
|
|
80
95
|
|
|
@@ -121,7 +136,7 @@ Extra filters to for pagination, output format (accept),
|
|
|
121
136
|
| Param | Type | Default | Description |
|
|
122
137
|
| ---------------------- | --------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
123
138
|
| `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`
|
|
139
|
+
| `limit` | `number` | API `1000` | Page size. Omit to use the API default. Max `6000`. |
|
|
125
140
|
| `cursor` | `string` | | Pagination cursor from a previous `meta.next_cursor`. |
|
|
126
141
|
| `disableBudgetWarning` | `boolean` | `false` | Ignore warnings for large queries that consume budget quotas. |
|
|
127
142
|
| `zoom` | `number` | | Map zoom hint (used by presets / server-side simplification policies). |
|
|
@@ -148,7 +163,7 @@ Fields for pagination and usage.
|
|
|
148
163
|
|
|
149
164
|
Auto-paginates (and optionally tiles the bbox) until the result is complete or a client-side cap is hit. GeoJSON only — for FlatGeobuf / other encodings, use `query` with `accept`.
|
|
150
165
|
|
|
151
|
-
Does not take `limit` or `cursor`; paging is handled internally.
|
|
166
|
+
Does not take `limit` or `cursor`; paging is handled internally. Does not tile when `within` is set (`bboxTiles` is ignored).
|
|
152
167
|
|
|
153
168
|
```ts
|
|
154
169
|
const all = await client.query_all({
|
|
@@ -168,12 +183,12 @@ console.log(all.meta.page_count, all.meta.has_more, all.meta.units_charged);
|
|
|
168
183
|
|
|
169
184
|
### Params
|
|
170
185
|
|
|
171
|
-
Same filter params as `query` (`bbox`, `tags`, `orTags`, `notTags`, `type`, `wayShape`, `zoom`, `location`, `radius`, `osmIds`, `minLengthM`, `maxLengthM`, `minAreaM2`, `maxAreaM2`, `centroid`, `clipGeometry`, `disableBudgetWarning`), plus:
|
|
186
|
+
Same filter params as `query` (`bbox`, `tags`, `orTags`, `notTags`, `type`, `wayShape`, `zoom`, `location`, `radius`, `within`, `osmIds`, `minLengthM`, `maxLengthM`, `minAreaM2`, `maxAreaM2`, `centroid`, `clipGeometry`, `disableBudgetWarning`), plus:
|
|
172
187
|
|
|
173
188
|
|
|
174
189
|
| Param | Type | Default | Description |
|
|
175
190
|
| -------------- | --------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
176
|
-
| `limitPerPage` | `number` | `1000`
|
|
191
|
+
| `limitPerPage` | `number` | API `1000` | Upstream `limit` per HTTP request (page size). Omit to use the API default. |
|
|
177
192
|
| `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
193
|
| `maxPages` | `number` | `15` | Max pages fetched **per tile**. |
|
|
179
194
|
| `maxFeatures` | `number | null` | `55000` | Cap on merged features after dedupe. Pass `null` for no cap. |
|
|
@@ -197,8 +212,33 @@ Same fields as `query`, plus:
|
|
|
197
212
|
| `relay_partial_reason` | e.g. `upstream_rejected_cursor` or `upstream_rate_limited_after_retries`. |
|
|
198
213
|
|
|
199
214
|
|
|
200
|
-
Also exports layer presets (`resolveLayerFromQuery`, `OSM_FEATURES_LAYER_PRESETS`, ...)
|
|
201
|
-
|
|
215
|
+
Also exports layer presets (`resolveLayerFromQuery`, `OSM_FEATURES_LAYER_PRESETS`, ...),
|
|
216
|
+
GeoJSON payload helpers (`featureCentroid`, `geometryBounds`, `parseFeatureId`, ...),
|
|
217
|
+
and local helpers (`nearest_within`, `point_in_geometry`, `isOpenNow`).
|
|
218
|
+
|
|
219
|
+
## `stats`
|
|
220
|
+
|
|
221
|
+
Count features grouped by a tag key via `GET /v2/osm_features/stats`. Returns `{ groups, total, truncated }`. Spatial windows are larger than `query` (country-scale on every tier) and billed count-only. `limit` is max histogram buckets (API default 100), not a scan cap. `groupBy` is required. Same tag filters as `query`; no `osmIds`, `cursor`, `zoom`, `centroid`, or `clipGeometry`. Map Express/query strings with `resolveStatsRequest` (requires `group_by`).
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
const histogram = await client.stats({
|
|
225
|
+
groupBy: 'amenity',
|
|
226
|
+
bbox: '18.05,59.32,18.10,59.34',
|
|
227
|
+
type: 'node',
|
|
228
|
+
tags: ['amenity'],
|
|
229
|
+
});
|
|
230
|
+
console.log(histogram.total, histogram.groups);
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
City boundary:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
const mix = await client.stats({
|
|
237
|
+
groupBy: 'amenity',
|
|
238
|
+
within: 'relation/398021',
|
|
239
|
+
tags: ['amenity'],
|
|
240
|
+
});
|
|
241
|
+
```
|
|
202
242
|
|
|
203
243
|
## `estimate_cost`
|
|
204
244
|
|
|
@@ -221,37 +261,15 @@ const usage = await client.usage();
|
|
|
221
261
|
console.log(usage.tier, usage.usage_this_month, usage.remaining_this_month);
|
|
222
262
|
```
|
|
223
263
|
|
|
224
|
-
##
|
|
225
|
-
|
|
226
|
-
`query()` is the generic OpenStreetMap layer: buildings, roads, park polygons, any tag and geometry shape. Geo Agent is the place and mobility layer on top of the same OSM data. You pick OSM tags (`amenity=cafe`), an area, a time, and walk or bike. The API returns coordinates, opening-hours status, nearest-first ranks, and walk or bike geometry.
|
|
227
|
-
|
|
228
|
-
These endpoints answer questions like "cafes near me", "bars open at 20:00", or "suggest a walking bar crawl in Stockholm". An AI agent or a script can call the same methods.
|
|
264
|
+
## Places and routes
|
|
229
265
|
|
|
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. |
|
|
266
|
+
`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.
|
|
238
267
|
|
|
239
|
-
|
|
268
|
+
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).
|
|
240
269
|
|
|
241
|
-
|
|
270
|
+
### Places search
|
|
242
271
|
|
|
243
|
-
|
|
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`
|
|
272
|
+
`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
273
|
|
|
256
274
|
```ts
|
|
257
275
|
const origin = { lat: 59.316, lon: 18.075 };
|
|
@@ -263,7 +281,13 @@ const cafes = await client.places_search({
|
|
|
263
281
|
openNow: true,
|
|
264
282
|
asOf: '2026-08-10T18:00:00+02:00',
|
|
265
283
|
});
|
|
284
|
+
```
|
|
266
285
|
|
|
286
|
+
### Nearby (ranked from a point)
|
|
287
|
+
|
|
288
|
+
`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.
|
|
289
|
+
|
|
290
|
+
```ts
|
|
267
291
|
const nearby = await client.places_nearby({
|
|
268
292
|
location: origin,
|
|
269
293
|
orTags: ['amenity=cafe'],
|
|
@@ -271,16 +295,55 @@ const nearby = await client.places_nearby({
|
|
|
271
295
|
openNow: true,
|
|
272
296
|
asOf: '2026-08-10T18:00:00+02:00',
|
|
273
297
|
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Place details
|
|
274
301
|
|
|
302
|
+
`places_details()` reloads a single OSM place by the id search or nearby gave you (`node/123`), or as `{ osmType, osmId }`.
|
|
303
|
+
|
|
304
|
+
```ts
|
|
275
305
|
const first = (cafes.features as { id: string }[])[0];
|
|
276
306
|
const details = await client.places_details({ osmType: first.id });
|
|
307
|
+
// same as: client.places_details({ osmType: 'node', osmId: 123 })
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Hours are evaluated at request time in that place’s timezone.
|
|
311
|
+
|
|
312
|
+
### Opening hours
|
|
313
|
+
|
|
314
|
+
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.
|
|
315
|
+
|
|
316
|
+
Timezone is inferred from coordinates (IANA). There is no `timezone` request field.
|
|
317
|
+
|
|
318
|
+
- `openNow: true` drops closed and unknown-hours POIs (Google Places–style `openNow`).
|
|
319
|
+
- `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.
|
|
320
|
+
- `asOf` or `openNow` also require an `opening_hours` tag, so untagged amenities do not pad the page.
|
|
321
|
+
- Places that are closed but tagged still appear unless `openNow` is set.
|
|
322
|
+
|
|
323
|
+
### "X near Y" (local join)
|
|
324
|
+
|
|
325
|
+
`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.
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
import { nearest_within } from 'osmfeatures';
|
|
329
|
+
|
|
330
|
+
const bbox = '18.05,59.33,18.10,59.36';
|
|
331
|
+
const restaurants = await client.places_search({ bbox, orTags: ['amenity=restaurant'] });
|
|
332
|
+
const stations = await client.places_search({ bbox, orTags: ['railway=station'] });
|
|
333
|
+
const pairs = nearest_within(restaurants, stations, 150, { limit: 20 });
|
|
334
|
+
|
|
335
|
+
for (const pair of pairs) {
|
|
336
|
+
console.log(pair.distance_m, pair.feature, 'near', pair.nearest);
|
|
337
|
+
}
|
|
277
338
|
```
|
|
278
339
|
|
|
279
|
-
|
|
340
|
+
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.
|
|
341
|
+
|
|
342
|
+
### Walk and bike routes
|
|
280
343
|
|
|
281
|
-
|
|
344
|
+
Paths follow OSM walk and bicycle ways. Default `travelMode` is `WALK`; pass `'BICYCLE'` for bikes. Driving is not offered yet.
|
|
282
345
|
|
|
283
|
-
|
|
346
|
+
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
347
|
|
|
285
348
|
```ts
|
|
286
349
|
const origin = { lon: 18.075, lat: 59.316 };
|
|
@@ -299,6 +362,15 @@ const tour = await client.routes_optimized_path({
|
|
|
299
362
|
start: origin,
|
|
300
363
|
stops: [cafe],
|
|
301
364
|
});
|
|
365
|
+
|
|
366
|
+
const office = { lon: 18.08, lat: 59.318 };
|
|
367
|
+
point_in_geometry(office.lon, office.lat, iso);
|
|
302
368
|
```
|
|
303
369
|
|
|
304
|
-
|
|
370
|
+
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.
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
## MCP server
|
|
374
|
+
|
|
375
|
+
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.
|
|
376
|
+
|
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
|
@@ -38,6 +38,7 @@ export type OSMFeaturesLayer = {
|
|
|
38
38
|
};
|
|
39
39
|
/** Flat query params (same idea as Python `query(**params)`). */
|
|
40
40
|
export type OSMFeaturesParams = OSMFeaturesLayer & {
|
|
41
|
+
/** Page size. Omit to use the API default (1000). Max `6000`. */
|
|
41
42
|
limit?: number;
|
|
42
43
|
cursor?: string;
|
|
43
44
|
zoom?: number;
|
|
@@ -45,18 +46,53 @@ export type OSMFeaturesParams = OSMFeaturesLayer & {
|
|
|
45
46
|
location?: string;
|
|
46
47
|
/** Search radius in metres. Requires `location`. */
|
|
47
48
|
radius?: number;
|
|
49
|
+
/** Polygon spatial anchor as `way/<id>` or `relation/<id>`. Mutually exclusive with `bbox` / `location`. */
|
|
50
|
+
within?: string;
|
|
48
51
|
osmIds?: string;
|
|
49
52
|
minLengthM?: number;
|
|
50
53
|
maxLengthM?: number;
|
|
51
54
|
minAreaM2?: number;
|
|
52
55
|
maxAreaM2?: number;
|
|
53
56
|
disableBudgetWarning?: boolean;
|
|
57
|
+
/** When true, include `properties.centroid` on non-point features. Omit for the API default (false). */
|
|
54
58
|
centroid?: boolean;
|
|
55
|
-
/** When true
|
|
59
|
+
/** When true, clip returned geometry to the requested bbox. Omit for the API default (false). */
|
|
56
60
|
clipGeometry?: boolean;
|
|
57
61
|
/** Accept media type. Default application/geo+json; other types put bytes in ``data``. */
|
|
58
62
|
accept?: string;
|
|
59
63
|
};
|
|
64
|
+
/** `GET /v2/osm_features/stats`. Same filters as `query` except paging/geometry extras. */
|
|
65
|
+
export type OSMFeaturesStatsParams = {
|
|
66
|
+
/** Tag key to group on (required). Features without this key are not counted. */
|
|
67
|
+
groupBy: string;
|
|
68
|
+
bbox?: string;
|
|
69
|
+
location?: string;
|
|
70
|
+
radius?: number;
|
|
71
|
+
within?: string;
|
|
72
|
+
type?: string;
|
|
73
|
+
wayShape?: 'line' | 'polygon' | 'all';
|
|
74
|
+
/** @deprecated Use `wayShape`. */
|
|
75
|
+
shape?: 'line' | 'polygon' | 'all';
|
|
76
|
+
tags?: string[];
|
|
77
|
+
orTags?: string[];
|
|
78
|
+
notTags?: string[];
|
|
79
|
+
/** Max histogram buckets (API default 100, max 10000). Does not cap the scan. */
|
|
80
|
+
limit?: number;
|
|
81
|
+
minLengthM?: number;
|
|
82
|
+
maxLengthM?: number;
|
|
83
|
+
minAreaM2?: number;
|
|
84
|
+
maxAreaM2?: number;
|
|
85
|
+
disableBudgetWarning?: boolean;
|
|
86
|
+
};
|
|
87
|
+
export type OSMStatsGroup = {
|
|
88
|
+
value: string;
|
|
89
|
+
count: number;
|
|
90
|
+
};
|
|
91
|
+
export type OSMStatsResponse = {
|
|
92
|
+
groups: OSMStatsGroup[];
|
|
93
|
+
total: number;
|
|
94
|
+
truncated: boolean;
|
|
95
|
+
};
|
|
60
96
|
type QueryValue = unknown;
|
|
61
97
|
export type OSMFeaturesQuery = Record<string, QueryValue>;
|
|
62
98
|
/** Routing point. Accepts ``lon`` or ``lng``. */
|
|
@@ -95,6 +131,7 @@ export type PlacesSearchParams = {
|
|
|
95
131
|
tags?: string[];
|
|
96
132
|
orTags?: string[];
|
|
97
133
|
limit?: number;
|
|
134
|
+
/** Keep only places known open at ``asOf`` (or now). Untagged hours are dropped. */
|
|
98
135
|
openNow?: boolean;
|
|
99
136
|
asOf?: string;
|
|
100
137
|
};
|
|
@@ -105,9 +142,29 @@ export type PlacesNearbyParams = {
|
|
|
105
142
|
tags?: string[];
|
|
106
143
|
orTags?: string[];
|
|
107
144
|
limit?: number;
|
|
145
|
+
/** Keep only places known open at ``asOf`` (or now). Untagged hours are dropped. */
|
|
108
146
|
openNow?: boolean;
|
|
109
147
|
asOf?: string;
|
|
110
148
|
};
|
|
149
|
+
/**
|
|
150
|
+
* Place GeoJSON ``properties`` from search / nearby / details.
|
|
151
|
+
* ``openNow`` is true/false when hours are evaluable at ``asOf`` (or now);
|
|
152
|
+
* omitted when missing or unparseable.
|
|
153
|
+
*/
|
|
154
|
+
export type PlaceProperties = {
|
|
155
|
+
tags?: Record<string, unknown>;
|
|
156
|
+
openNow?: boolean;
|
|
157
|
+
centroid?: unknown;
|
|
158
|
+
};
|
|
159
|
+
export type PlaceFeatureLike = {
|
|
160
|
+
properties?: PlaceProperties | Record<string, unknown>;
|
|
161
|
+
/** ``places_details`` envelope: ``{ status, feature }``. */
|
|
162
|
+
feature?: PlaceFeatureLike;
|
|
163
|
+
};
|
|
164
|
+
/** ``properties.openNow === true`` (known open). Missing / non-boolean is not open. Unwraps ``{ feature }``. */
|
|
165
|
+
export declare function isOpenNow(feature: PlaceFeatureLike | undefined): boolean;
|
|
166
|
+
/** ``true`` / ``false`` when hours are known; ``undefined`` when the field is omitted. Unwraps ``{ feature }``. */
|
|
167
|
+
export declare function readOpenNow(feature: PlaceFeatureLike | undefined): boolean | undefined;
|
|
111
168
|
export type PlacesDetailsParams = {
|
|
112
169
|
/** ``node`` / ``way`` / ``relation``, or a full ``node/123`` feature id. */
|
|
113
170
|
osmType: string;
|
|
@@ -138,6 +195,8 @@ export declare class OSMFeatures {
|
|
|
138
195
|
});
|
|
139
196
|
/** Map Express/query params + resolved layer into flat `query` / `query_all` params. */
|
|
140
197
|
resolveRequest(query: OSMFeaturesQuery, layer: OSMFeaturesLayer): OSMFeaturesParams;
|
|
198
|
+
/** Map Express/query params into `stats` params. `group_by` is required. */
|
|
199
|
+
resolveStatsRequest(query: OSMFeaturesQuery | URLSearchParams): OSMFeaturesStatsParams;
|
|
141
200
|
private throwUpstreamError;
|
|
142
201
|
/** GET/POST with 429 retry. Throws on non-OK. */
|
|
143
202
|
private _fetchOk;
|
|
@@ -148,25 +207,27 @@ export declare class OSMFeatures {
|
|
|
148
207
|
/** Single HTTP request with retry. Throws on non-OK (same role as Python `_raw_query`). */
|
|
149
208
|
private _rawQuery;
|
|
150
209
|
/** Single upstream page. Params map 1:1 to server query string (no tiling). */
|
|
151
|
-
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>;
|
|
210
|
+
query({ bbox, tags, orTags, notTags, type, wayShape, shape, limit, cursor, zoom, location, radius, within, osmIds, minLengthM, maxLengthM, minAreaM2, maxAreaM2, disableBudgetWarning, centroid, clipGeometry, accept, }: OSMFeaturesParams, dependencies?: OSMFeaturesDependencies): Promise<OSMFeaturesResult>;
|
|
152
211
|
/** Auto-paginate (and optionally tile) until complete. Each page uses `_rawQuery`. */
|
|
153
|
-
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'> & {
|
|
154
|
-
/** Upstream `limit` per HTTP request (page size). */
|
|
212
|
+
query_all({ bbox, tags, orTags, notTags, type, wayShape, shape, zoom, location, radius, within, osmIds, minLengthM, maxLengthM, minAreaM2, maxAreaM2, disableBudgetWarning, centroid, clipGeometry, accept, limitPerPage, bboxTiles, maxPages, maxFeatures, }: Omit<OSMFeaturesParams, 'limit' | 'cursor'> & {
|
|
213
|
+
/** Upstream `limit` per HTTP request (page size). Omit to use the API default (1000). */
|
|
155
214
|
limitPerPage?: number;
|
|
156
215
|
bboxTiles?: number;
|
|
157
216
|
maxPages?: number;
|
|
158
217
|
/** Cap on merged features. `null` = no cap. */
|
|
159
218
|
maxFeatures?: number | null;
|
|
160
219
|
}, dependencies?: OSMFeaturesDependencies): Promise<OSMGeoJSONResult>;
|
|
220
|
+
/** Count features grouped by a tag key (``GET /v2/osm_features/stats``). */
|
|
221
|
+
stats(params: OSMFeaturesStatsParams, dependencies?: OSMFeaturesDependencies): Promise<OSMStatsResponse>;
|
|
161
222
|
/** Preflight credit cost (``GET /v2/osm_features/cost``). */
|
|
162
223
|
estimate_cost(params?: OSMFeaturesParams, dependencies?: OSMFeaturesDependencies): Promise<Record<string, unknown>>;
|
|
163
224
|
/** This month's unit-budget usage (``GET /v1/usage``). */
|
|
164
225
|
usage(dependencies?: OSMFeaturesDependencies): Promise<Record<string, unknown>>;
|
|
165
|
-
/** Find places in a bbox or radius (``POST /v1/places/search``). */
|
|
226
|
+
/** Find places in a bbox or radius (``POST /v1/places/search``). Features have ``properties.openNow`` when hours are known. */
|
|
166
227
|
places_search(params: PlacesSearchParams, dependencies?: OSMFeaturesDependencies): Promise<Record<string, unknown>>;
|
|
167
|
-
/** Nearest places from a point (``POST /v1/places/nearby``). */
|
|
228
|
+
/** Nearest places from a point (``POST /v1/places/nearby``). Features have ``properties.openNow`` when hours are known. */
|
|
168
229
|
places_nearby(params: PlacesNearbyParams, dependencies?: OSMFeaturesDependencies): Promise<Record<string, unknown>>;
|
|
169
|
-
/** One place by OSM id (``GET /v1/places/{osm_type}/{osm_id}``). */
|
|
230
|
+
/** One place by OSM id (``GET /v1/places/{osm_type}/{osm_id}``). Feature has ``properties.openNow`` when hours are known. */
|
|
170
231
|
places_details(params: PlacesDetailsParams, dependencies?: OSMFeaturesDependencies): Promise<Record<string, unknown>>;
|
|
171
232
|
/** Reach polygon along the walk/bike network (``POST /v1/routes/isochrone``). */
|
|
172
233
|
routes_isochrone(params: RoutesIsochroneParams, dependencies?: OSMFeaturesDependencies): Promise<Record<string, unknown>>;
|
|
@@ -176,4 +237,6 @@ export declare class OSMFeatures {
|
|
|
176
237
|
routes_optimized_path(params: RoutesOptimizedPathParams, dependencies?: OSMFeaturesDependencies): Promise<Record<string, unknown>>;
|
|
177
238
|
}
|
|
178
239
|
export * from './geojson-feature.js';
|
|
240
|
+
export * from './geometry.js';
|
|
241
|
+
export * from './nearest.js';
|
|
179
242
|
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,43 @@ function optionalBoolean(query, key) {
|
|
|
88
109
|
}
|
|
89
110
|
return undefined;
|
|
90
111
|
}
|
|
91
|
-
function
|
|
112
|
+
function stringList(query, key) {
|
|
113
|
+
const raw = query[key];
|
|
114
|
+
if (raw == null || raw === '') {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
const values = Array.isArray(raw) ? raw : [raw];
|
|
118
|
+
const out = [];
|
|
119
|
+
for (const value of values) {
|
|
120
|
+
if (value == null || value === '') {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const trimmed = String(value).trim();
|
|
124
|
+
if (trimmed) {
|
|
125
|
+
out.push(trimmed);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
function queryFromSearchParams(params) {
|
|
131
|
+
const out = {};
|
|
132
|
+
for (const key of new Set(params.keys())) {
|
|
133
|
+
const all = params.getAll(key);
|
|
134
|
+
out[key] = all.length <= 1 ? (all[0] ?? '') : all;
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
function asQueryMap(query) {
|
|
139
|
+
return query instanceof URLSearchParams ? queryFromSearchParams(query) : query;
|
|
140
|
+
}
|
|
141
|
+
function parseLimit(query) {
|
|
92
142
|
const raw = optionalString(query, 'limit');
|
|
93
143
|
if (raw == null) {
|
|
94
|
-
return
|
|
144
|
+
return undefined;
|
|
95
145
|
}
|
|
96
146
|
const parsed = Number.parseInt(raw, 10);
|
|
97
147
|
if (!Number.isFinite(parsed)) {
|
|
98
|
-
return
|
|
148
|
+
return undefined;
|
|
99
149
|
}
|
|
100
150
|
if (parsed > MAX_LIMIT) {
|
|
101
151
|
throw appError(400, 'invalid_limit', `limit must be <= ${MAX_LIMIT}.`);
|
|
@@ -149,7 +199,9 @@ function buildFeaturesQuery(params) {
|
|
|
149
199
|
if (params.bbox) {
|
|
150
200
|
query.set('bbox', params.bbox);
|
|
151
201
|
}
|
|
152
|
-
|
|
202
|
+
if (params.limit != null) {
|
|
203
|
+
query.set('limit', String(params.limit));
|
|
204
|
+
}
|
|
153
205
|
if (params.cursor) {
|
|
154
206
|
query.set('cursor', params.cursor);
|
|
155
207
|
}
|
|
@@ -162,6 +214,12 @@ function buildFeaturesQuery(params) {
|
|
|
162
214
|
if (params.radius != null) {
|
|
163
215
|
query.set('radius', String(params.radius));
|
|
164
216
|
}
|
|
217
|
+
if (params.within) {
|
|
218
|
+
query.set('within', params.within);
|
|
219
|
+
}
|
|
220
|
+
if (params.groupBy) {
|
|
221
|
+
query.set('group_by', params.groupBy);
|
|
222
|
+
}
|
|
165
223
|
if (params.osmIds) {
|
|
166
224
|
query.set('osm_ids', params.osmIds);
|
|
167
225
|
}
|
|
@@ -177,10 +235,10 @@ function buildFeaturesQuery(params) {
|
|
|
177
235
|
if (params.maxAreaM2 != null) {
|
|
178
236
|
query.set('max_area_m2', String(params.maxAreaM2));
|
|
179
237
|
}
|
|
180
|
-
if (params.disableBudgetWarning
|
|
238
|
+
if (params.disableBudgetWarning) {
|
|
181
239
|
query.set('disable_budget_warning', String(params.disableBudgetWarning));
|
|
182
240
|
}
|
|
183
|
-
if (params.centroid
|
|
241
|
+
if (params.centroid) {
|
|
184
242
|
query.set('centroid', String(params.centroid));
|
|
185
243
|
}
|
|
186
244
|
if (params.clipGeometry != null) {
|
|
@@ -232,7 +290,7 @@ function parsePlaceRef(osmType, osmId) {
|
|
|
232
290
|
return { osmType: kind, osmId: n };
|
|
233
291
|
}
|
|
234
292
|
function placesSearchBody(params) {
|
|
235
|
-
const body = {
|
|
293
|
+
const body = {};
|
|
236
294
|
if (params.bbox != null) {
|
|
237
295
|
body['bbox'] = params.bbox;
|
|
238
296
|
}
|
|
@@ -251,6 +309,9 @@ function placesSearchBody(params) {
|
|
|
251
309
|
if (params.orTags?.length) {
|
|
252
310
|
body['orTags'] = params.orTags;
|
|
253
311
|
}
|
|
312
|
+
if (params.limit != null) {
|
|
313
|
+
body['limit'] = params.limit;
|
|
314
|
+
}
|
|
254
315
|
if (params.openNow) {
|
|
255
316
|
body['openNow'] = true;
|
|
256
317
|
}
|
|
@@ -262,9 +323,10 @@ function placesSearchBody(params) {
|
|
|
262
323
|
function placesNearbyBody(params) {
|
|
263
324
|
const body = {
|
|
264
325
|
location: latlng(params.location),
|
|
265
|
-
radius: params.radius ?? 1000,
|
|
266
|
-
limit: params.limit ?? 10,
|
|
267
326
|
};
|
|
327
|
+
if (params.radius != null) {
|
|
328
|
+
body['radius'] = params.radius;
|
|
329
|
+
}
|
|
268
330
|
if (params.type != null) {
|
|
269
331
|
body['type'] = params.type;
|
|
270
332
|
}
|
|
@@ -274,6 +336,9 @@ function placesNearbyBody(params) {
|
|
|
274
336
|
if (params.orTags?.length) {
|
|
275
337
|
body['orTags'] = params.orTags;
|
|
276
338
|
}
|
|
339
|
+
if (params.limit != null) {
|
|
340
|
+
body['limit'] = params.limit;
|
|
341
|
+
}
|
|
277
342
|
if (params.openNow) {
|
|
278
343
|
body['openNow'] = true;
|
|
279
344
|
}
|
|
@@ -285,7 +350,6 @@ function placesNearbyBody(params) {
|
|
|
285
350
|
function routesIsochroneBody(params) {
|
|
286
351
|
const body = {
|
|
287
352
|
origin: lonlat(params.origin),
|
|
288
|
-
travelMode: params.travelMode ?? 'WALK',
|
|
289
353
|
};
|
|
290
354
|
if (params.maxDistanceM != null) {
|
|
291
355
|
body['max_distance_m'] = params.maxDistanceM;
|
|
@@ -296,28 +360,37 @@ function routesIsochroneBody(params) {
|
|
|
296
360
|
if (params.searchBufferM != null) {
|
|
297
361
|
body['search_buffer_m'] = params.searchBufferM;
|
|
298
362
|
}
|
|
363
|
+
if (params.travelMode != null) {
|
|
364
|
+
body['travelMode'] = params.travelMode;
|
|
365
|
+
}
|
|
299
366
|
return body;
|
|
300
367
|
}
|
|
301
368
|
function routesPathBody(params) {
|
|
302
369
|
const body = {
|
|
303
370
|
stops: params.stops.map(lonlat),
|
|
304
|
-
travelMode: params.travelMode ?? 'WALK',
|
|
305
371
|
};
|
|
306
372
|
if (params.searchBufferM != null) {
|
|
307
373
|
body['search_buffer_m'] = params.searchBufferM;
|
|
308
374
|
}
|
|
375
|
+
if (params.travelMode != null) {
|
|
376
|
+
body['travelMode'] = params.travelMode;
|
|
377
|
+
}
|
|
309
378
|
return body;
|
|
310
379
|
}
|
|
311
380
|
function routesOptimizedPathBody(params) {
|
|
312
381
|
const body = {
|
|
313
382
|
start: lonlat(params.start),
|
|
314
383
|
stops: params.stops.map(lonlat),
|
|
315
|
-
loop: params.loop ?? true,
|
|
316
|
-
travelMode: params.travelMode ?? 'WALK',
|
|
317
384
|
};
|
|
385
|
+
if (params.loop != null) {
|
|
386
|
+
body['loop'] = params.loop;
|
|
387
|
+
}
|
|
318
388
|
if (params.searchBufferM != null) {
|
|
319
389
|
body['search_buffer_m'] = params.searchBufferM;
|
|
320
390
|
}
|
|
391
|
+
if (params.travelMode != null) {
|
|
392
|
+
body['travelMode'] = params.travelMode;
|
|
393
|
+
}
|
|
321
394
|
return body;
|
|
322
395
|
}
|
|
323
396
|
function sleep(ms) {
|
|
@@ -390,6 +463,7 @@ export class OSMFeatures {
|
|
|
390
463
|
zoom: optionalNumber(query, 'zoom'),
|
|
391
464
|
location: optionalString(query, 'location'),
|
|
392
465
|
radius: optionalNumber(query, 'radius'),
|
|
466
|
+
within: optionalString(query, 'within'),
|
|
393
467
|
osmIds: optionalString(query, 'osm_ids'),
|
|
394
468
|
minLengthM: optionalNumber(query, 'min_length_m'),
|
|
395
469
|
maxLengthM: optionalNumber(query, 'max_length_m'),
|
|
@@ -400,6 +474,64 @@ export class OSMFeatures {
|
|
|
400
474
|
clipGeometry: optionalBoolean(query, 'clipGeometry'),
|
|
401
475
|
};
|
|
402
476
|
}
|
|
477
|
+
/** Map Express/query params into `stats` params. `group_by` is required. */
|
|
478
|
+
resolveStatsRequest(query) {
|
|
479
|
+
const q = asQueryMap(query);
|
|
480
|
+
const groupBy = optionalString(q, 'group_by');
|
|
481
|
+
if (groupBy == null) {
|
|
482
|
+
throw appError(400, 'invalid_group_by', 'group_by is required.');
|
|
483
|
+
}
|
|
484
|
+
const tags = stringList(q, 'tags');
|
|
485
|
+
const orTags = stringList(q, 'or_tags');
|
|
486
|
+
const notTags = stringList(q, 'not_tags');
|
|
487
|
+
const wayShapeRaw = optionalString(q, 'way_shape') ?? optionalString(q, 'shape');
|
|
488
|
+
const wayShape = wayShapeRaw === 'line' || wayShapeRaw === 'polygon' || wayShapeRaw === 'all'
|
|
489
|
+
? wayShapeRaw
|
|
490
|
+
: undefined;
|
|
491
|
+
const params = { groupBy };
|
|
492
|
+
const bbox = optionalString(q, 'bbox');
|
|
493
|
+
if (bbox)
|
|
494
|
+
params.bbox = bbox;
|
|
495
|
+
const within = optionalString(q, 'within');
|
|
496
|
+
if (within)
|
|
497
|
+
params.within = within;
|
|
498
|
+
const location = optionalString(q, 'location');
|
|
499
|
+
if (location)
|
|
500
|
+
params.location = location;
|
|
501
|
+
const radius = optionalNumber(q, 'radius');
|
|
502
|
+
if (radius != null)
|
|
503
|
+
params.radius = radius;
|
|
504
|
+
const type = optionalString(q, 'type');
|
|
505
|
+
if (type)
|
|
506
|
+
params.type = type;
|
|
507
|
+
if (wayShape)
|
|
508
|
+
params.wayShape = wayShape;
|
|
509
|
+
if (tags.length > 0)
|
|
510
|
+
params.tags = tags;
|
|
511
|
+
if (orTags.length > 0)
|
|
512
|
+
params.orTags = orTags;
|
|
513
|
+
if (notTags.length > 0)
|
|
514
|
+
params.notTags = notTags;
|
|
515
|
+
const limit = optionalNumber(q, 'limit');
|
|
516
|
+
if (limit != null && Number.isFinite(limit))
|
|
517
|
+
params.limit = Math.trunc(limit);
|
|
518
|
+
const minLengthM = optionalNumber(q, 'min_length_m');
|
|
519
|
+
if (minLengthM != null)
|
|
520
|
+
params.minLengthM = minLengthM;
|
|
521
|
+
const maxLengthM = optionalNumber(q, 'max_length_m');
|
|
522
|
+
if (maxLengthM != null)
|
|
523
|
+
params.maxLengthM = maxLengthM;
|
|
524
|
+
const minAreaM2 = optionalNumber(q, 'min_area_m2');
|
|
525
|
+
if (minAreaM2 != null)
|
|
526
|
+
params.minAreaM2 = minAreaM2;
|
|
527
|
+
const maxAreaM2 = optionalNumber(q, 'max_area_m2');
|
|
528
|
+
if (maxAreaM2 != null)
|
|
529
|
+
params.maxAreaM2 = maxAreaM2;
|
|
530
|
+
const disableBudgetWarning = optionalBoolean(q, 'disable_budget_warning');
|
|
531
|
+
if (disableBudgetWarning)
|
|
532
|
+
params.disableBudgetWarning = true;
|
|
533
|
+
return params;
|
|
534
|
+
}
|
|
403
535
|
async throwUpstreamError(upstream) {
|
|
404
536
|
const subtype = upstream.status === 429 ? 'upstream_rate_limit' : undefined;
|
|
405
537
|
const upstreamDetail = await readUpstreamErrorDetail(upstream);
|
|
@@ -519,7 +651,7 @@ export class OSMFeatures {
|
|
|
519
651
|
};
|
|
520
652
|
}
|
|
521
653
|
/** Single upstream page. Params map 1:1 to server query string (no tiling). */
|
|
522
|
-
async query({ bbox, tags, orTags, notTags, type, wayShape, shape, limit
|
|
654
|
+
async query({ bbox, tags, orTags, notTags, type, wayShape, shape, limit, cursor, zoom, location, radius, within, osmIds, minLengthM, maxLengthM, minAreaM2, maxAreaM2, disableBudgetWarning, centroid, clipGeometry, accept, }, dependencies = {}) {
|
|
523
655
|
const payload = await this._rawQuery({
|
|
524
656
|
bbox,
|
|
525
657
|
tags,
|
|
@@ -533,6 +665,7 @@ export class OSMFeatures {
|
|
|
533
665
|
zoom,
|
|
534
666
|
location,
|
|
535
667
|
radius,
|
|
668
|
+
within,
|
|
536
669
|
osmIds,
|
|
537
670
|
minLengthM,
|
|
538
671
|
maxLengthM,
|
|
@@ -546,7 +679,7 @@ export class OSMFeatures {
|
|
|
546
679
|
return payload;
|
|
547
680
|
}
|
|
548
681
|
/** Auto-paginate (and optionally tile) until complete. Each page uses `_rawQuery`. */
|
|
549
|
-
async query_all({ bbox, tags, orTags, notTags, type, wayShape, shape, zoom, location, radius, osmIds, minLengthM, maxLengthM, minAreaM2, maxAreaM2, disableBudgetWarning, centroid, clipGeometry, accept, limitPerPage
|
|
682
|
+
async query_all({ bbox, tags, orTags, notTags, type, wayShape, shape, zoom, location, radius, within, osmIds, minLengthM, maxLengthM, minAreaM2, maxAreaM2, disableBudgetWarning, centroid, clipGeometry, accept, limitPerPage, bboxTiles = 2, maxPages = 15, maxFeatures = 55_000, }, dependencies = {}) {
|
|
550
683
|
if (!isGeojsonAccept(accept)) {
|
|
551
684
|
throw appError(400, 'invalid_accept', 'query_all only supports GeoJSON; use query({ accept }) for binary encodings.');
|
|
552
685
|
}
|
|
@@ -557,7 +690,7 @@ export class OSMFeatures {
|
|
|
557
690
|
const sleepFn = dependencies.sleepFn ?? sleep;
|
|
558
691
|
const nowFn = dependencies.nowFn ?? Date.now;
|
|
559
692
|
const featureCap = maxFeatures == null ? Number.POSITIVE_INFINITY : maxFeatures;
|
|
560
|
-
const tileBboxes = bbox ? splitBbox(bbox, bboxTiles)
|
|
693
|
+
const tileBboxes = within || !bbox ? [undefined] : splitBbox(bbox, bboxTiles);
|
|
561
694
|
const allFeatures = [];
|
|
562
695
|
let pageCount = 0;
|
|
563
696
|
let lastPage = null;
|
|
@@ -578,6 +711,7 @@ export class OSMFeatures {
|
|
|
578
711
|
zoom,
|
|
579
712
|
location,
|
|
580
713
|
radius,
|
|
714
|
+
within,
|
|
581
715
|
osmIds,
|
|
582
716
|
minLengthM,
|
|
583
717
|
maxLengthM,
|
|
@@ -601,7 +735,7 @@ export class OSMFeatures {
|
|
|
601
735
|
while (tilePages < maxPages && allFeatures.length < featureCap) {
|
|
602
736
|
let page;
|
|
603
737
|
try {
|
|
604
|
-
const raw = await this._rawQuery({ ...baseParams, bbox: tileBbox, cursor }, fetchFn, sleepFn, nowFn);
|
|
738
|
+
const raw = await this._rawQuery({ ...baseParams, bbox: within ? undefined : tileBbox, cursor }, fetchFn, sleepFn, nowFn);
|
|
605
739
|
if (raw.data instanceof ArrayBuffer) {
|
|
606
740
|
throw appError(400, 'invalid_accept', 'query_all only supports GeoJSON; use query({ accept }) for binary encodings.');
|
|
607
741
|
}
|
|
@@ -665,6 +799,31 @@ export class OSMFeatures {
|
|
|
665
799
|
}
|
|
666
800
|
return resultFromFeatures(features, meta);
|
|
667
801
|
}
|
|
802
|
+
/** Count features grouped by a tag key (``GET /v2/osm_features/stats``). */
|
|
803
|
+
async stats(params, dependencies = {}) {
|
|
804
|
+
if (!params.groupBy) {
|
|
805
|
+
throw appError(400, 'invalid_group_by', 'group_by is required.');
|
|
806
|
+
}
|
|
807
|
+
const body = await this._getJson('/v2/osm_features/stats', buildFeaturesQuery({
|
|
808
|
+
bbox: params.bbox,
|
|
809
|
+
tags: params.tags,
|
|
810
|
+
orTags: params.orTags,
|
|
811
|
+
notTags: params.notTags,
|
|
812
|
+
type: params.type,
|
|
813
|
+
wayShape: params.wayShape ?? params.shape,
|
|
814
|
+
limit: params.limit,
|
|
815
|
+
location: params.location,
|
|
816
|
+
radius: params.radius,
|
|
817
|
+
within: params.within,
|
|
818
|
+
groupBy: params.groupBy,
|
|
819
|
+
minLengthM: params.minLengthM,
|
|
820
|
+
maxLengthM: params.maxLengthM,
|
|
821
|
+
minAreaM2: params.minAreaM2,
|
|
822
|
+
maxAreaM2: params.maxAreaM2,
|
|
823
|
+
disableBudgetWarning: params.disableBudgetWarning,
|
|
824
|
+
}), dependencies.fetchFn ?? fetch, dependencies.sleepFn ?? sleep, dependencies.nowFn ?? Date.now);
|
|
825
|
+
return body;
|
|
826
|
+
}
|
|
668
827
|
/** Preflight credit cost (``GET /v2/osm_features/cost``). */
|
|
669
828
|
async estimate_cost(params = {}, dependencies = {}) {
|
|
670
829
|
return this._getJson('/v2/osm_features/cost', buildFeaturesQuery({
|
|
@@ -674,10 +833,11 @@ export class OSMFeatures {
|
|
|
674
833
|
notTags: params.notTags,
|
|
675
834
|
type: params.type,
|
|
676
835
|
wayShape: params.wayShape ?? params.shape,
|
|
677
|
-
limit: params.limit
|
|
836
|
+
limit: params.limit,
|
|
678
837
|
zoom: params.zoom,
|
|
679
838
|
location: params.location,
|
|
680
839
|
radius: params.radius,
|
|
840
|
+
within: params.within,
|
|
681
841
|
osmIds: params.osmIds,
|
|
682
842
|
minLengthM: params.minLengthM,
|
|
683
843
|
maxLengthM: params.maxLengthM,
|
|
@@ -692,15 +852,15 @@ export class OSMFeatures {
|
|
|
692
852
|
async usage(dependencies = {}) {
|
|
693
853
|
return this._getJson('/v1/usage', {}, dependencies.fetchFn ?? fetch, dependencies.sleepFn ?? sleep, dependencies.nowFn ?? Date.now);
|
|
694
854
|
}
|
|
695
|
-
/** Find places in a bbox or radius (``POST /v1/places/search``). */
|
|
855
|
+
/** Find places in a bbox or radius (``POST /v1/places/search``). Features have ``properties.openNow`` when hours are known. */
|
|
696
856
|
async places_search(params, dependencies = {}) {
|
|
697
857
|
return this._postJson('/v1/places/search', placesSearchBody(params), dependencies.fetchFn ?? fetch, dependencies.sleepFn ?? sleep, dependencies.nowFn ?? Date.now);
|
|
698
858
|
}
|
|
699
|
-
/** Nearest places from a point (``POST /v1/places/nearby``). */
|
|
859
|
+
/** Nearest places from a point (``POST /v1/places/nearby``). Features have ``properties.openNow`` when hours are known. */
|
|
700
860
|
async places_nearby(params, dependencies = {}) {
|
|
701
861
|
return this._postJson('/v1/places/nearby', placesNearbyBody(params), dependencies.fetchFn ?? fetch, dependencies.sleepFn ?? sleep, dependencies.nowFn ?? Date.now);
|
|
702
862
|
}
|
|
703
|
-
/** One place by OSM id (``GET /v1/places/{osm_type}/{osm_id}``). */
|
|
863
|
+
/** One place by OSM id (``GET /v1/places/{osm_type}/{osm_id}``). Feature has ``properties.openNow`` when hours are known. */
|
|
704
864
|
async places_details(params, dependencies = {}) {
|
|
705
865
|
const { osmType, osmId } = parsePlaceRef(params.osmType, params.osmId);
|
|
706
866
|
return this._getJson(`/v1/places/${osmType}/${osmId}`, {}, dependencies.fetchFn ?? fetch, dependencies.sleepFn ?? sleep, dependencies.nowFn ?? Date.now);
|
|
@@ -719,4 +879,6 @@ export class OSMFeatures {
|
|
|
719
879
|
}
|
|
720
880
|
}
|
|
721
881
|
export * from './geojson-feature.js';
|
|
882
|
+
export * from './geometry.js';
|
|
883
|
+
export * from './nearest.js';
|
|
722
884
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "osmfeatures",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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 && tsx src/tutorial.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
|
},
|