osmfeatures 0.1.2 → 0.2.1
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 +78 -2
- package/dist/index.d.ts +83 -4
- package/dist/index.js +240 -26
- package/dist/preset/catalog.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,7 +73,8 @@ The geographical area for the request in terms of GPS coordinates or specific OS
|
|
|
73
73
|
| Param | Type | Description |
|
|
74
74
|
| -------- | -------- | -------------------------------------------------- |
|
|
75
75
|
| `bbox` | `string` | Bounding box as `min_lon,min_lat,max_lon,max_lat`. |
|
|
76
|
-
| `
|
|
76
|
+
| `location` | `string` | Point for a radius search as `lat,lng`. Requires `radius`. |
|
|
77
|
+
| `radius` | `number` | Search radius in metres. Requires `location`. |
|
|
77
78
|
| `osmIds` | `string` | Comma-separated OSM IDs to fetch by id. |
|
|
78
79
|
|
|
79
80
|
|
|
@@ -167,7 +168,7 @@ console.log(all.meta.page_count, all.meta.has_more, all.meta.units_charged);
|
|
|
167
168
|
|
|
168
169
|
### Params
|
|
169
170
|
|
|
170
|
-
Same filter params as `query` (`bbox`, `tags`, `orTags`, `notTags`, `type`, `shape`, `zoom`, `
|
|
171
|
+
Same filter params as `query` (`bbox`, `tags`, `orTags`, `notTags`, `type`, `shape`, `zoom`, `location`, `radius`, `osmIds`, `minLengthM`, `maxLengthM`, `minAreaM2`, `maxAreaM2`, `centroid`, `clipGeometry`, `disableBudgetWarning`), plus:
|
|
171
172
|
|
|
172
173
|
|
|
173
174
|
| Param | Type | Default | Description |
|
|
@@ -199,4 +200,79 @@ Same fields as `query`, plus:
|
|
|
199
200
|
Also exports layer presets (`resolveLayerFromQuery`, `OSM_FEATURES_LAYER_PRESETS`, ...)
|
|
200
201
|
and GeoJSON payload helpers (`featureCentroid`, `geometryBounds`, `parseFeatureId`, ...).
|
|
201
202
|
|
|
203
|
+
## `estimate_cost`
|
|
204
|
+
|
|
205
|
+
Preflight credit cost via `GET /v2/osm_features/cost`. Same filter params as `query`. No OSM data is fetched.
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
const estimate = await client.estimate_cost({
|
|
209
|
+
bbox: '18.06,59.32,18.09,59.34',
|
|
210
|
+
tags: ['building'],
|
|
211
|
+
});
|
|
212
|
+
console.log(estimate.estimated_credits);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## `usage`
|
|
216
|
+
|
|
217
|
+
This month's unit-budget usage via `GET /v1/usage`.
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
const usage = await client.usage();
|
|
221
|
+
console.log(usage.tier, usage.usage_this_month, usage.remaining_this_month);
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## `places_search` / `places_nearby` / `places_details`
|
|
225
|
+
|
|
226
|
+
Place discovery and lookup. Search is a bbox or `location`+`radius`. Nearby ranks one set from a point. Details refetches a search/nearby feature id (`node/123`). Search and nearby hours use each place's local timezone; optional `asOf` pins the evaluation instant.
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
const origin = { lat: 59.316, lon: 18.075 };
|
|
230
|
+
|
|
231
|
+
const cafes = await client.places_search({
|
|
232
|
+
location: origin,
|
|
233
|
+
radius: 800,
|
|
234
|
+
orTags: ['amenity=cafe'],
|
|
235
|
+
openNow: true,
|
|
236
|
+
asOf: '2026-08-10T18:00:00+02:00',
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const nearby = await client.places_nearby({
|
|
240
|
+
location: origin,
|
|
241
|
+
orTags: ['amenity=cafe'],
|
|
242
|
+
limit: 5,
|
|
243
|
+
openNow: true,
|
|
244
|
+
asOf: '2026-08-10T18:00:00+02:00',
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const first = (cafes.features as { id: string }[])[0];
|
|
248
|
+
const details = await client.places_details({ osmType: first.id });
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`places_details` also accepts `{ osmType: 'node', osmId: 123 }`. Hours are annotated at request time in the place's local timezone.
|
|
252
|
+
|
|
253
|
+
## `routes_isochrone` / `routes_path` / `routes_optimized_path`
|
|
254
|
+
|
|
255
|
+
Walk or bicycle routing via `POST /v1/routes/*`. Points accept `lon` or `lng`.
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
const origin = { lon: 18.075, lat: 59.316 };
|
|
259
|
+
const cafe = { lon: 18.08, lat: 59.318 };
|
|
260
|
+
|
|
261
|
+
const iso = await client.routes_isochrone({
|
|
262
|
+
origin,
|
|
263
|
+
durationS: 600,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const path = await client.routes_path({
|
|
267
|
+
stops: [origin, cafe],
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const tour = await client.routes_optimized_path({
|
|
271
|
+
start: origin,
|
|
272
|
+
stops: [cafe],
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
`routes_isochrone` takes exactly one of `maxDistanceM` or `durationS`. `routes_path` follows `stops` in listed order (no TSP). `routes_optimized_path` orders `stops` from `start`; `loop` (default true) returns to start. Optional `searchBufferM` and `travelMode` (`WALK` or `BICYCLE`).
|
|
277
|
+
|
|
202
278
|
Read the full API reference here [https://maplark.com/developer](https://maplark.com/developer) such as the OpenAPI 2.0 HTTP docs.
|
package/dist/index.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export type OSMFeaturesResult = {
|
|
|
27
27
|
};
|
|
28
28
|
/** Layer filters from presets / custom resolve. Pass into `resolveRequest` or spread into `query`. */
|
|
29
29
|
export type OSMFeaturesLayer = {
|
|
30
|
-
bbox
|
|
30
|
+
bbox?: string;
|
|
31
31
|
tags?: string[];
|
|
32
32
|
orTags?: string[];
|
|
33
33
|
notTags?: string[];
|
|
@@ -39,7 +39,10 @@ export type OSMFeaturesParams = OSMFeaturesLayer & {
|
|
|
39
39
|
limit?: number;
|
|
40
40
|
cursor?: string;
|
|
41
41
|
zoom?: number;
|
|
42
|
-
|
|
42
|
+
/** Point for a radius search as `lat,lng`. Requires `radius`. */
|
|
43
|
+
location?: string;
|
|
44
|
+
/** Search radius in metres. Requires `location`. */
|
|
45
|
+
radius?: number;
|
|
43
46
|
osmIds?: string;
|
|
44
47
|
minLengthM?: number;
|
|
45
48
|
maxLengthM?: number;
|
|
@@ -54,6 +57,60 @@ export type OSMFeaturesParams = OSMFeaturesLayer & {
|
|
|
54
57
|
};
|
|
55
58
|
type QueryValue = unknown;
|
|
56
59
|
export type OSMFeaturesQuery = Record<string, QueryValue>;
|
|
60
|
+
/** Routing point. Accepts ``lon`` or ``lng``. */
|
|
61
|
+
export type LonLat = {
|
|
62
|
+
lat: number;
|
|
63
|
+
} & ({
|
|
64
|
+
lon: number;
|
|
65
|
+
} | {
|
|
66
|
+
lng: number;
|
|
67
|
+
});
|
|
68
|
+
export type RouteTravelMode = 'WALK' | 'BICYCLE';
|
|
69
|
+
export type RoutesIsochroneParams = {
|
|
70
|
+
origin: LonLat;
|
|
71
|
+
maxDistanceM?: number;
|
|
72
|
+
durationS?: number;
|
|
73
|
+
searchBufferM?: number;
|
|
74
|
+
travelMode?: RouteTravelMode;
|
|
75
|
+
};
|
|
76
|
+
export type RoutesPathParams = {
|
|
77
|
+
stops: LonLat[];
|
|
78
|
+
searchBufferM?: number;
|
|
79
|
+
travelMode?: RouteTravelMode;
|
|
80
|
+
};
|
|
81
|
+
export type RoutesOptimizedPathParams = {
|
|
82
|
+
start: LonLat;
|
|
83
|
+
stops: LonLat[];
|
|
84
|
+
searchBufferM?: number;
|
|
85
|
+
loop?: boolean;
|
|
86
|
+
travelMode?: RouteTravelMode;
|
|
87
|
+
};
|
|
88
|
+
export type PlacesSearchParams = {
|
|
89
|
+
bbox?: string;
|
|
90
|
+
location?: LonLat;
|
|
91
|
+
radius?: number;
|
|
92
|
+
type?: string;
|
|
93
|
+
tags?: string[];
|
|
94
|
+
orTags?: string[];
|
|
95
|
+
limit?: number;
|
|
96
|
+
openNow?: boolean;
|
|
97
|
+
asOf?: string;
|
|
98
|
+
};
|
|
99
|
+
export type PlacesNearbyParams = {
|
|
100
|
+
location: LonLat;
|
|
101
|
+
radius?: number;
|
|
102
|
+
type?: string;
|
|
103
|
+
tags?: string[];
|
|
104
|
+
orTags?: string[];
|
|
105
|
+
limit?: number;
|
|
106
|
+
openNow?: boolean;
|
|
107
|
+
asOf?: string;
|
|
108
|
+
};
|
|
109
|
+
export type PlacesDetailsParams = {
|
|
110
|
+
/** ``node`` / ``way`` / ``relation``, or a full ``node/123`` feature id. */
|
|
111
|
+
osmType: string;
|
|
112
|
+
osmId?: number | string;
|
|
113
|
+
};
|
|
57
114
|
type OSMFeaturesDependencies = {
|
|
58
115
|
fetchFn?: typeof fetch;
|
|
59
116
|
sleepFn?: (ms: number) => Promise<void>;
|
|
@@ -80,12 +137,18 @@ export declare class OSMFeatures {
|
|
|
80
137
|
/** Map Express/query params + resolved layer into flat `query` / `query_all` params. */
|
|
81
138
|
resolveRequest(query: OSMFeaturesQuery, layer: OSMFeaturesLayer): OSMFeaturesParams;
|
|
82
139
|
private throwUpstreamError;
|
|
140
|
+
/** GET/POST with 429 retry. Throws on non-OK. */
|
|
141
|
+
private _fetchOk;
|
|
142
|
+
/** POST JSON to a geo-agent path. */
|
|
143
|
+
private _postJson;
|
|
144
|
+
/** GET JSON from a geo-agent or account path. */
|
|
145
|
+
private _getJson;
|
|
83
146
|
/** Single HTTP request with retry. Throws on non-OK (same role as Python `_raw_query`). */
|
|
84
147
|
private _rawQuery;
|
|
85
148
|
/** Single upstream page. Params map 1:1 to server query string (no tiling). */
|
|
86
|
-
query({ bbox, tags, orTags, notTags, type, shape, limit, cursor, zoom,
|
|
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>;
|
|
87
150
|
/** Auto-paginate (and optionally tile) until complete. Each page uses `_rawQuery`. */
|
|
88
|
-
query_all({ bbox, tags, orTags, notTags, type, shape, zoom,
|
|
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'> & {
|
|
89
152
|
/** Upstream `limit` per HTTP request (page size). */
|
|
90
153
|
limitPerPage?: number;
|
|
91
154
|
bboxTiles?: number;
|
|
@@ -93,6 +156,22 @@ export declare class OSMFeatures {
|
|
|
93
156
|
/** Cap on merged features. `null` = no cap. */
|
|
94
157
|
maxFeatures?: number | null;
|
|
95
158
|
}, dependencies?: OSMFeaturesDependencies): Promise<OSMGeoJSONResult>;
|
|
159
|
+
/** Preflight credit cost (``GET /v2/osm_features/cost``). */
|
|
160
|
+
estimate_cost(params?: OSMFeaturesParams, dependencies?: OSMFeaturesDependencies): Promise<Record<string, unknown>>;
|
|
161
|
+
/** This month's unit-budget usage (``GET /v1/usage``). */
|
|
162
|
+
usage(dependencies?: OSMFeaturesDependencies): Promise<Record<string, unknown>>;
|
|
163
|
+
/** Find places in a bbox or radius (``POST /v1/places/search``). */
|
|
164
|
+
places_search(params: PlacesSearchParams, dependencies?: OSMFeaturesDependencies): Promise<Record<string, unknown>>;
|
|
165
|
+
/** Nearest places from a point (``POST /v1/places/nearby``). */
|
|
166
|
+
places_nearby(params: PlacesNearbyParams, dependencies?: OSMFeaturesDependencies): Promise<Record<string, unknown>>;
|
|
167
|
+
/** One place by OSM id (``GET /v1/places/{osm_type}/{osm_id}``). */
|
|
168
|
+
places_details(params: PlacesDetailsParams, dependencies?: OSMFeaturesDependencies): Promise<Record<string, unknown>>;
|
|
169
|
+
/** Reach polygon along the walk/bike network (``POST /v1/routes/isochrone``). */
|
|
170
|
+
routes_isochrone(params: RoutesIsochroneParams, dependencies?: OSMFeaturesDependencies): Promise<Record<string, unknown>>;
|
|
171
|
+
/** Given-order walk/bike path (``POST /v1/routes/path``). */
|
|
172
|
+
routes_path(params: RoutesPathParams, dependencies?: OSMFeaturesDependencies): Promise<Record<string, unknown>>;
|
|
173
|
+
/** TSP walk/bike tour from ``start`` (``POST /v1/routes/optimized_path``). ``loop`` returns to start. */
|
|
174
|
+
routes_optimized_path(params: RoutesOptimizedPathParams, dependencies?: OSMFeaturesDependencies): Promise<Record<string, unknown>>;
|
|
96
175
|
}
|
|
97
176
|
export * from './geojson-feature.js';
|
|
98
177
|
export * from './preset/index.js';
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ function metaFromHeaders(headers, featureCount) {
|
|
|
21
21
|
}
|
|
22
22
|
return meta;
|
|
23
23
|
}
|
|
24
|
+
const PLACE_TYPES = new Set(['node', 'way', 'relation']);
|
|
24
25
|
const DEFAULT_BASE_URL = 'https://api.maplark.com';
|
|
25
26
|
const DEFAULT_LIMIT = 1000;
|
|
26
27
|
const MAX_LIMIT = 6000;
|
|
@@ -145,7 +146,9 @@ export function splitBbox(bbox, tileCount) {
|
|
|
145
146
|
}
|
|
146
147
|
function buildFeaturesQuery(params) {
|
|
147
148
|
const query = new URLSearchParams();
|
|
148
|
-
|
|
149
|
+
if (params.bbox) {
|
|
150
|
+
query.set('bbox', params.bbox);
|
|
151
|
+
}
|
|
149
152
|
query.set('limit', String(params.limit));
|
|
150
153
|
if (params.cursor) {
|
|
151
154
|
query.set('cursor', params.cursor);
|
|
@@ -153,8 +156,11 @@ function buildFeaturesQuery(params) {
|
|
|
153
156
|
if (params.zoom != null) {
|
|
154
157
|
query.set('zoom', String(params.zoom));
|
|
155
158
|
}
|
|
156
|
-
if (params.
|
|
157
|
-
query.set('
|
|
159
|
+
if (params.location) {
|
|
160
|
+
query.set('location', params.location);
|
|
161
|
+
}
|
|
162
|
+
if (params.radius != null) {
|
|
163
|
+
query.set('radius', String(params.radius));
|
|
158
164
|
}
|
|
159
165
|
if (params.osmIds) {
|
|
160
166
|
query.set('osm_ids', params.osmIds);
|
|
@@ -197,6 +203,122 @@ function buildFeaturesQuery(params) {
|
|
|
197
203
|
}
|
|
198
204
|
return query;
|
|
199
205
|
}
|
|
206
|
+
function lonlat(point) {
|
|
207
|
+
const lon = 'lon' in point ? point.lon : point.lng;
|
|
208
|
+
return { lon, lat: point.lat };
|
|
209
|
+
}
|
|
210
|
+
function latlng(point) {
|
|
211
|
+
const lng = 'lng' in point ? point.lng : point.lon;
|
|
212
|
+
return { lat: point.lat, lng };
|
|
213
|
+
}
|
|
214
|
+
function parsePlaceRef(osmType, osmId) {
|
|
215
|
+
if (osmId == null) {
|
|
216
|
+
const raw = osmType.trim();
|
|
217
|
+
const slash = raw.indexOf('/');
|
|
218
|
+
if (slash < 0) {
|
|
219
|
+
throw appError(400, 'invalid_place_id', 'place id must be node|way|relation plus a positive osm_id');
|
|
220
|
+
}
|
|
221
|
+
return parsePlaceRef(raw.slice(0, slash), raw.slice(slash + 1));
|
|
222
|
+
}
|
|
223
|
+
const kind = osmType.trim().toLowerCase();
|
|
224
|
+
if (!PLACE_TYPES.has(kind)) {
|
|
225
|
+
throw appError(400, 'invalid_place_id', 'osm_type must be node, way, or relation');
|
|
226
|
+
}
|
|
227
|
+
const n = typeof osmId === 'number' ? osmId : Number.parseInt(String(osmId).trim(), 10);
|
|
228
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
229
|
+
throw appError(400, 'invalid_place_id', 'osm_id must be a positive integer');
|
|
230
|
+
}
|
|
231
|
+
return { osmType: kind, osmId: n };
|
|
232
|
+
}
|
|
233
|
+
function placesSearchBody(params) {
|
|
234
|
+
const body = { limit: params.limit ?? 100 };
|
|
235
|
+
if (params.bbox != null) {
|
|
236
|
+
body['bbox'] = params.bbox;
|
|
237
|
+
}
|
|
238
|
+
if (params.location != null) {
|
|
239
|
+
body['location'] = latlng(params.location);
|
|
240
|
+
}
|
|
241
|
+
if (params.radius != null) {
|
|
242
|
+
body['radius'] = params.radius;
|
|
243
|
+
}
|
|
244
|
+
if (params.type != null) {
|
|
245
|
+
body['type'] = params.type;
|
|
246
|
+
}
|
|
247
|
+
if (params.tags?.length) {
|
|
248
|
+
body['tags'] = params.tags;
|
|
249
|
+
}
|
|
250
|
+
if (params.orTags?.length) {
|
|
251
|
+
body['orTags'] = params.orTags;
|
|
252
|
+
}
|
|
253
|
+
if (params.openNow) {
|
|
254
|
+
body['openNow'] = true;
|
|
255
|
+
}
|
|
256
|
+
if (params.asOf != null) {
|
|
257
|
+
body['asOf'] = params.asOf;
|
|
258
|
+
}
|
|
259
|
+
return body;
|
|
260
|
+
}
|
|
261
|
+
function placesNearbyBody(params) {
|
|
262
|
+
const body = {
|
|
263
|
+
location: latlng(params.location),
|
|
264
|
+
radius: params.radius ?? 1000,
|
|
265
|
+
limit: params.limit ?? 10,
|
|
266
|
+
};
|
|
267
|
+
if (params.type != null) {
|
|
268
|
+
body['type'] = params.type;
|
|
269
|
+
}
|
|
270
|
+
if (params.tags?.length) {
|
|
271
|
+
body['tags'] = params.tags;
|
|
272
|
+
}
|
|
273
|
+
if (params.orTags?.length) {
|
|
274
|
+
body['orTags'] = params.orTags;
|
|
275
|
+
}
|
|
276
|
+
if (params.openNow) {
|
|
277
|
+
body['openNow'] = true;
|
|
278
|
+
}
|
|
279
|
+
if (params.asOf != null) {
|
|
280
|
+
body['asOf'] = params.asOf;
|
|
281
|
+
}
|
|
282
|
+
return body;
|
|
283
|
+
}
|
|
284
|
+
function routesIsochroneBody(params) {
|
|
285
|
+
const body = {
|
|
286
|
+
origin: lonlat(params.origin),
|
|
287
|
+
travelMode: params.travelMode ?? 'WALK',
|
|
288
|
+
};
|
|
289
|
+
if (params.maxDistanceM != null) {
|
|
290
|
+
body['max_distance_m'] = params.maxDistanceM;
|
|
291
|
+
}
|
|
292
|
+
if (params.durationS != null) {
|
|
293
|
+
body['duration_s'] = params.durationS;
|
|
294
|
+
}
|
|
295
|
+
if (params.searchBufferM != null) {
|
|
296
|
+
body['search_buffer_m'] = params.searchBufferM;
|
|
297
|
+
}
|
|
298
|
+
return body;
|
|
299
|
+
}
|
|
300
|
+
function routesPathBody(params) {
|
|
301
|
+
const body = {
|
|
302
|
+
stops: params.stops.map(lonlat),
|
|
303
|
+
travelMode: params.travelMode ?? 'WALK',
|
|
304
|
+
};
|
|
305
|
+
if (params.searchBufferM != null) {
|
|
306
|
+
body['search_buffer_m'] = params.searchBufferM;
|
|
307
|
+
}
|
|
308
|
+
return body;
|
|
309
|
+
}
|
|
310
|
+
function routesOptimizedPathBody(params) {
|
|
311
|
+
const body = {
|
|
312
|
+
start: lonlat(params.start),
|
|
313
|
+
stops: params.stops.map(lonlat),
|
|
314
|
+
loop: params.loop ?? true,
|
|
315
|
+
travelMode: params.travelMode ?? 'WALK',
|
|
316
|
+
};
|
|
317
|
+
if (params.searchBufferM != null) {
|
|
318
|
+
body['search_buffer_m'] = params.searchBufferM;
|
|
319
|
+
}
|
|
320
|
+
return body;
|
|
321
|
+
}
|
|
200
322
|
function sleep(ms) {
|
|
201
323
|
return new Promise((resolve) => {
|
|
202
324
|
setTimeout(resolve, ms);
|
|
@@ -265,7 +387,8 @@ export class OSMFeatures {
|
|
|
265
387
|
limit: parseLimit(query),
|
|
266
388
|
cursor: optionalString(query, 'cursor'),
|
|
267
389
|
zoom: optionalNumber(query, 'zoom'),
|
|
268
|
-
|
|
390
|
+
location: optionalString(query, 'location'),
|
|
391
|
+
radius: optionalNumber(query, 'radius'),
|
|
269
392
|
osmIds: optionalString(query, 'osm_ids'),
|
|
270
393
|
minLengthM: optionalNumber(query, 'min_length_m'),
|
|
271
394
|
maxLengthM: optionalNumber(query, 'max_length_m'),
|
|
@@ -287,25 +410,15 @@ export class OSMFeatures {
|
|
|
287
410
|
err.upstreamDetail = upstreamDetail;
|
|
288
411
|
throw err;
|
|
289
412
|
}
|
|
290
|
-
/**
|
|
291
|
-
async
|
|
292
|
-
const query = buildFeaturesQuery(params);
|
|
293
|
-
const upstreamUrl = new URL(`${this.apiBaseUrl}/v2/osm_features`);
|
|
294
|
-
for (const [key, value] of query.entries()) {
|
|
295
|
-
upstreamUrl.searchParams.append(key, value);
|
|
296
|
-
}
|
|
297
|
-
let upstream;
|
|
413
|
+
/** GET/POST with 429 retry. Throws on non-OK. */
|
|
414
|
+
async _fetchOk(url, init, fetchFn, sleepFn, nowFn) {
|
|
298
415
|
let retryAttempt = 0;
|
|
299
416
|
while (true) {
|
|
417
|
+
let upstream;
|
|
300
418
|
try {
|
|
301
|
-
upstream = await fetchFn(
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
305
|
-
Accept: params.accept || GEOJSON_ACCEPT,
|
|
306
|
-
'User-Agent': 'osmfeatures',
|
|
307
|
-
},
|
|
308
|
-
signal: AbortSignal.timeout(this.timeoutMs),
|
|
419
|
+
upstream = await fetchFn(url, {
|
|
420
|
+
...init,
|
|
421
|
+
signal: init.signal ?? AbortSignal.timeout(this.timeoutMs),
|
|
309
422
|
});
|
|
310
423
|
}
|
|
311
424
|
catch (error) {
|
|
@@ -317,7 +430,7 @@ export class OSMFeatures {
|
|
|
317
430
|
throw err;
|
|
318
431
|
}
|
|
319
432
|
if (upstream.ok) {
|
|
320
|
-
|
|
433
|
+
return upstream;
|
|
321
434
|
}
|
|
322
435
|
if (upstream.status === 429 && retryAttempt < this.retryAttempts) {
|
|
323
436
|
retryAttempt += 1;
|
|
@@ -346,6 +459,53 @@ export class OSMFeatures {
|
|
|
346
459
|
}
|
|
347
460
|
await this.throwUpstreamError(upstream);
|
|
348
461
|
}
|
|
462
|
+
}
|
|
463
|
+
/** POST JSON to a geo-agent path. */
|
|
464
|
+
async _postJson(path, body, fetchFn, sleepFn, nowFn) {
|
|
465
|
+
const upstream = await this._fetchOk(`${this.apiBaseUrl}${path}`, {
|
|
466
|
+
method: 'POST',
|
|
467
|
+
headers: {
|
|
468
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
469
|
+
Accept: 'application/json',
|
|
470
|
+
'Content-Type': 'application/json',
|
|
471
|
+
'User-Agent': 'osmfeatures',
|
|
472
|
+
},
|
|
473
|
+
body: JSON.stringify(body),
|
|
474
|
+
}, fetchFn, sleepFn, nowFn);
|
|
475
|
+
return (await upstream.json());
|
|
476
|
+
}
|
|
477
|
+
/** GET JSON from a geo-agent or account path. */
|
|
478
|
+
async _getJson(path, query, fetchFn, sleepFn, nowFn) {
|
|
479
|
+
const url = new URL(`${this.apiBaseUrl}${path}`);
|
|
480
|
+
const entries = query instanceof URLSearchParams ? query.entries() : Object.entries(query);
|
|
481
|
+
for (const [key, value] of entries) {
|
|
482
|
+
url.searchParams.append(key, value);
|
|
483
|
+
}
|
|
484
|
+
const upstream = await this._fetchOk(url.toString(), {
|
|
485
|
+
method: 'GET',
|
|
486
|
+
headers: {
|
|
487
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
488
|
+
Accept: 'application/json',
|
|
489
|
+
'User-Agent': 'osmfeatures',
|
|
490
|
+
},
|
|
491
|
+
}, fetchFn, sleepFn, nowFn);
|
|
492
|
+
return (await upstream.json());
|
|
493
|
+
}
|
|
494
|
+
/** Single HTTP request with retry. Throws on non-OK (same role as Python `_raw_query`). */
|
|
495
|
+
async _rawQuery(params, fetchFn, sleepFn, nowFn) {
|
|
496
|
+
const query = buildFeaturesQuery(params);
|
|
497
|
+
const upstreamUrl = new URL(`${this.apiBaseUrl}/v2/osm_features`);
|
|
498
|
+
for (const [key, value] of query.entries()) {
|
|
499
|
+
upstreamUrl.searchParams.append(key, value);
|
|
500
|
+
}
|
|
501
|
+
const upstream = await this._fetchOk(upstreamUrl.toString(), {
|
|
502
|
+
method: 'GET',
|
|
503
|
+
headers: {
|
|
504
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
505
|
+
Accept: params.accept || GEOJSON_ACCEPT,
|
|
506
|
+
'User-Agent': 'osmfeatures',
|
|
507
|
+
},
|
|
508
|
+
}, fetchFn, sleepFn, nowFn);
|
|
349
509
|
if (isGeojsonAccept(params.accept)) {
|
|
350
510
|
const body = (await upstream.json());
|
|
351
511
|
const features = Array.isArray(body.features) ? body.features : [];
|
|
@@ -358,7 +518,7 @@ export class OSMFeatures {
|
|
|
358
518
|
};
|
|
359
519
|
}
|
|
360
520
|
/** Single upstream page. Params map 1:1 to server query string (no tiling). */
|
|
361
|
-
async query({ bbox, tags, orTags, notTags, type, shape, limit = DEFAULT_LIMIT, cursor, zoom,
|
|
521
|
+
async query({ bbox, tags, orTags, notTags, type, shape, limit = DEFAULT_LIMIT, cursor, zoom, location, radius, osmIds, minLengthM, maxLengthM, minAreaM2, maxAreaM2, disableBudgetWarning, centroid, clipGeometry, accept, }, dependencies = {}) {
|
|
362
522
|
const payload = await this._rawQuery({
|
|
363
523
|
bbox,
|
|
364
524
|
tags,
|
|
@@ -369,7 +529,8 @@ export class OSMFeatures {
|
|
|
369
529
|
limit,
|
|
370
530
|
cursor,
|
|
371
531
|
zoom,
|
|
372
|
-
|
|
532
|
+
location,
|
|
533
|
+
radius,
|
|
373
534
|
osmIds,
|
|
374
535
|
minLengthM,
|
|
375
536
|
maxLengthM,
|
|
@@ -383,7 +544,7 @@ export class OSMFeatures {
|
|
|
383
544
|
return payload;
|
|
384
545
|
}
|
|
385
546
|
/** Auto-paginate (and optionally tile) until complete. Each page uses `_rawQuery`. */
|
|
386
|
-
async query_all({ bbox, tags, orTags, notTags, type, shape, zoom,
|
|
547
|
+
async query_all({ bbox, tags, orTags, notTags, type, shape, zoom, location, radius, osmIds, minLengthM, maxLengthM, minAreaM2, maxAreaM2, disableBudgetWarning, centroid, clipGeometry, accept, limitPerPage = DEFAULT_LIMIT, bboxTiles = 2, maxPages = 15, maxFeatures = 55_000, }, dependencies = {}) {
|
|
387
548
|
if (!isGeojsonAccept(accept)) {
|
|
388
549
|
throw appError(400, 'invalid_accept', 'query_all only supports GeoJSON; use query({ accept }) for binary encodings.');
|
|
389
550
|
}
|
|
@@ -394,7 +555,7 @@ export class OSMFeatures {
|
|
|
394
555
|
const sleepFn = dependencies.sleepFn ?? sleep;
|
|
395
556
|
const nowFn = dependencies.nowFn ?? Date.now;
|
|
396
557
|
const featureCap = maxFeatures == null ? Number.POSITIVE_INFINITY : maxFeatures;
|
|
397
|
-
const tileBboxes = splitBbox(bbox, bboxTiles);
|
|
558
|
+
const tileBboxes = bbox ? splitBbox(bbox, bboxTiles) : [undefined];
|
|
398
559
|
const allFeatures = [];
|
|
399
560
|
let pageCount = 0;
|
|
400
561
|
let lastPage = null;
|
|
@@ -412,7 +573,8 @@ export class OSMFeatures {
|
|
|
412
573
|
shape,
|
|
413
574
|
limit: limitPerPage,
|
|
414
575
|
zoom,
|
|
415
|
-
|
|
576
|
+
location,
|
|
577
|
+
radius,
|
|
416
578
|
osmIds,
|
|
417
579
|
minLengthM,
|
|
418
580
|
maxLengthM,
|
|
@@ -500,6 +662,58 @@ export class OSMFeatures {
|
|
|
500
662
|
}
|
|
501
663
|
return resultFromFeatures(features, meta);
|
|
502
664
|
}
|
|
665
|
+
/** Preflight credit cost (``GET /v2/osm_features/cost``). */
|
|
666
|
+
async estimate_cost(params = {}, dependencies = {}) {
|
|
667
|
+
return this._getJson('/v2/osm_features/cost', buildFeaturesQuery({
|
|
668
|
+
bbox: params.bbox,
|
|
669
|
+
tags: params.tags,
|
|
670
|
+
orTags: params.orTags,
|
|
671
|
+
notTags: params.notTags,
|
|
672
|
+
type: params.type,
|
|
673
|
+
shape: params.shape,
|
|
674
|
+
limit: params.limit ?? DEFAULT_LIMIT,
|
|
675
|
+
zoom: params.zoom,
|
|
676
|
+
location: params.location,
|
|
677
|
+
radius: params.radius,
|
|
678
|
+
osmIds: params.osmIds,
|
|
679
|
+
minLengthM: params.minLengthM,
|
|
680
|
+
maxLengthM: params.maxLengthM,
|
|
681
|
+
minAreaM2: params.minAreaM2,
|
|
682
|
+
maxAreaM2: params.maxAreaM2,
|
|
683
|
+
disableBudgetWarning: params.disableBudgetWarning,
|
|
684
|
+
centroid: params.centroid,
|
|
685
|
+
clipGeometry: params.clipGeometry,
|
|
686
|
+
}), dependencies.fetchFn ?? fetch, dependencies.sleepFn ?? sleep, dependencies.nowFn ?? Date.now);
|
|
687
|
+
}
|
|
688
|
+
/** This month's unit-budget usage (``GET /v1/usage``). */
|
|
689
|
+
async usage(dependencies = {}) {
|
|
690
|
+
return this._getJson('/v1/usage', {}, dependencies.fetchFn ?? fetch, dependencies.sleepFn ?? sleep, dependencies.nowFn ?? Date.now);
|
|
691
|
+
}
|
|
692
|
+
/** Find places in a bbox or radius (``POST /v1/places/search``). */
|
|
693
|
+
async places_search(params, dependencies = {}) {
|
|
694
|
+
return this._postJson('/v1/places/search', placesSearchBody(params), dependencies.fetchFn ?? fetch, dependencies.sleepFn ?? sleep, dependencies.nowFn ?? Date.now);
|
|
695
|
+
}
|
|
696
|
+
/** Nearest places from a point (``POST /v1/places/nearby``). */
|
|
697
|
+
async places_nearby(params, dependencies = {}) {
|
|
698
|
+
return this._postJson('/v1/places/nearby', placesNearbyBody(params), dependencies.fetchFn ?? fetch, dependencies.sleepFn ?? sleep, dependencies.nowFn ?? Date.now);
|
|
699
|
+
}
|
|
700
|
+
/** One place by OSM id (``GET /v1/places/{osm_type}/{osm_id}``). */
|
|
701
|
+
async places_details(params, dependencies = {}) {
|
|
702
|
+
const { osmType, osmId } = parsePlaceRef(params.osmType, params.osmId);
|
|
703
|
+
return this._getJson(`/v1/places/${osmType}/${osmId}`, {}, dependencies.fetchFn ?? fetch, dependencies.sleepFn ?? sleep, dependencies.nowFn ?? Date.now);
|
|
704
|
+
}
|
|
705
|
+
/** Reach polygon along the walk/bike network (``POST /v1/routes/isochrone``). */
|
|
706
|
+
async routes_isochrone(params, dependencies = {}) {
|
|
707
|
+
return this._postJson('/v1/routes/isochrone', routesIsochroneBody(params), dependencies.fetchFn ?? fetch, dependencies.sleepFn ?? sleep, dependencies.nowFn ?? Date.now);
|
|
708
|
+
}
|
|
709
|
+
/** Given-order walk/bike path (``POST /v1/routes/path``). */
|
|
710
|
+
async routes_path(params, dependencies = {}) {
|
|
711
|
+
return this._postJson('/v1/routes/path', routesPathBody(params), dependencies.fetchFn ?? fetch, dependencies.sleepFn ?? sleep, dependencies.nowFn ?? Date.now);
|
|
712
|
+
}
|
|
713
|
+
/** TSP walk/bike tour from ``start`` (``POST /v1/routes/optimized_path``). ``loop`` returns to start. */
|
|
714
|
+
async routes_optimized_path(params, dependencies = {}) {
|
|
715
|
+
return this._postJson('/v1/routes/optimized_path', routesOptimizedPathBody(params), dependencies.fetchFn ?? fetch, dependencies.sleepFn ?? sleep, dependencies.nowFn ?? Date.now);
|
|
716
|
+
}
|
|
503
717
|
}
|
|
504
718
|
export * from './geojson-feature.js';
|
|
505
719
|
export * from './preset/index.js';
|
package/dist/preset/catalog.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "osmfeatures",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|