osmfeatures 0.3.0 → 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 CHANGED
@@ -10,6 +10,7 @@ The backend is dedicated PostGIS, not the public Overpass endpoint, with API key
10
10
  - [Functions and Parameters](#functions-and-parameters)
11
11
  - [query()](#query)
12
12
  - [query_all](#query_all)
13
+ - [stats](#stats)
13
14
  - [estimate_cost](#estimate_cost)
14
15
  - [usage](#usage)
15
16
  - [Places and routes](#places-and-routes)
@@ -88,6 +89,7 @@ The geographical area for the request in terms of GPS coordinates or specific OS
88
89
  | `bbox` | `string` | Bounding box as `min_lon,min_lat,max_lon,max_lat`. |
89
90
  | `location` | `string` | Point for a radius search as `lat,lng`. Requires `radius`. |
90
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`. |
91
93
  | `osmIds` | `string` | Comma-separated OSM IDs to fetch by id. |
92
94
 
93
95
 
@@ -161,7 +163,7 @@ Fields for pagination and usage.
161
163
 
162
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`.
163
165
 
164
- 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).
165
167
 
166
168
  ```ts
167
169
  const all = await client.query_all({
@@ -181,7 +183,7 @@ console.log(all.meta.page_count, all.meta.has_more, all.meta.units_charged);
181
183
 
182
184
  ### Params
183
185
 
184
- 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:
185
187
 
186
188
 
187
189
  | Param | Type | Default | Description |
@@ -214,6 +216,30 @@ Also exports layer presets (`resolveLayerFromQuery`, `OSM_FEATURES_LAYER_PRESETS
214
216
  GeoJSON payload helpers (`featureCentroid`, `geometryBounds`, `parseFeatureId`, ...),
215
217
  and local helpers (`nearest_within`, `point_in_geometry`, `isOpenNow`).
216
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
+ ```
242
+
217
243
  ## `estimate_cost`
218
244
 
219
245
  Preflight credit cost via `GET /v2/osm_features/cost`. Same filter params as `query`. No OSM data is fetched.
package/dist/index.d.ts CHANGED
@@ -46,6 +46,8 @@ export type OSMFeaturesParams = OSMFeaturesLayer & {
46
46
  location?: string;
47
47
  /** Search radius in metres. Requires `location`. */
48
48
  radius?: number;
49
+ /** Polygon spatial anchor as `way/<id>` or `relation/<id>`. Mutually exclusive with `bbox` / `location`. */
50
+ within?: string;
49
51
  osmIds?: string;
50
52
  minLengthM?: number;
51
53
  maxLengthM?: number;
@@ -59,6 +61,38 @@ export type OSMFeaturesParams = OSMFeaturesLayer & {
59
61
  /** Accept media type. Default application/geo+json; other types put bytes in ``data``. */
60
62
  accept?: string;
61
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
+ };
62
96
  type QueryValue = unknown;
63
97
  export type OSMFeaturesQuery = Record<string, QueryValue>;
64
98
  /** Routing point. Accepts ``lon`` or ``lng``. */
@@ -161,6 +195,8 @@ export declare class OSMFeatures {
161
195
  });
162
196
  /** Map Express/query params + resolved layer into flat `query` / `query_all` params. */
163
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;
164
200
  private throwUpstreamError;
165
201
  /** GET/POST with 429 retry. Throws on non-OK. */
166
202
  private _fetchOk;
@@ -171,9 +207,9 @@ export declare class OSMFeatures {
171
207
  /** Single HTTP request with retry. Throws on non-OK (same role as Python `_raw_query`). */
172
208
  private _rawQuery;
173
209
  /** Single upstream page. Params map 1:1 to server query string (no tiling). */
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>;
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>;
175
211
  /** Auto-paginate (and optionally tile) until complete. Each page uses `_rawQuery`. */
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'> & {
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'> & {
177
213
  /** Upstream `limit` per HTTP request (page size). Omit to use the API default (1000). */
178
214
  limitPerPage?: number;
179
215
  bboxTiles?: number;
@@ -181,6 +217,8 @@ export declare class OSMFeatures {
181
217
  /** Cap on merged features. `null` = no cap. */
182
218
  maxFeatures?: number | null;
183
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>;
184
222
  /** Preflight credit cost (``GET /v2/osm_features/cost``). */
185
223
  estimate_cost(params?: OSMFeaturesParams, dependencies?: OSMFeaturesDependencies): Promise<Record<string, unknown>>;
186
224
  /** This month's unit-budget usage (``GET /v1/usage``). */
package/dist/index.js CHANGED
@@ -109,6 +109,35 @@ function optionalBoolean(query, key) {
109
109
  }
110
110
  return undefined;
111
111
  }
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
+ }
112
141
  function parseLimit(query) {
113
142
  const raw = optionalString(query, 'limit');
114
143
  if (raw == null) {
@@ -185,6 +214,12 @@ function buildFeaturesQuery(params) {
185
214
  if (params.radius != null) {
186
215
  query.set('radius', String(params.radius));
187
216
  }
217
+ if (params.within) {
218
+ query.set('within', params.within);
219
+ }
220
+ if (params.groupBy) {
221
+ query.set('group_by', params.groupBy);
222
+ }
188
223
  if (params.osmIds) {
189
224
  query.set('osm_ids', params.osmIds);
190
225
  }
@@ -428,6 +463,7 @@ export class OSMFeatures {
428
463
  zoom: optionalNumber(query, 'zoom'),
429
464
  location: optionalString(query, 'location'),
430
465
  radius: optionalNumber(query, 'radius'),
466
+ within: optionalString(query, 'within'),
431
467
  osmIds: optionalString(query, 'osm_ids'),
432
468
  minLengthM: optionalNumber(query, 'min_length_m'),
433
469
  maxLengthM: optionalNumber(query, 'max_length_m'),
@@ -438,6 +474,64 @@ export class OSMFeatures {
438
474
  clipGeometry: optionalBoolean(query, 'clipGeometry'),
439
475
  };
440
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
+ }
441
535
  async throwUpstreamError(upstream) {
442
536
  const subtype = upstream.status === 429 ? 'upstream_rate_limit' : undefined;
443
537
  const upstreamDetail = await readUpstreamErrorDetail(upstream);
@@ -557,7 +651,7 @@ export class OSMFeatures {
557
651
  };
558
652
  }
559
653
  /** Single upstream page. Params map 1:1 to server query string (no tiling). */
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 = {}) {
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 = {}) {
561
655
  const payload = await this._rawQuery({
562
656
  bbox,
563
657
  tags,
@@ -571,6 +665,7 @@ export class OSMFeatures {
571
665
  zoom,
572
666
  location,
573
667
  radius,
668
+ within,
574
669
  osmIds,
575
670
  minLengthM,
576
671
  maxLengthM,
@@ -584,7 +679,7 @@ export class OSMFeatures {
584
679
  return payload;
585
680
  }
586
681
  /** Auto-paginate (and optionally tile) until complete. Each page uses `_rawQuery`. */
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 = {}) {
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 = {}) {
588
683
  if (!isGeojsonAccept(accept)) {
589
684
  throw appError(400, 'invalid_accept', 'query_all only supports GeoJSON; use query({ accept }) for binary encodings.');
590
685
  }
@@ -595,7 +690,7 @@ export class OSMFeatures {
595
690
  const sleepFn = dependencies.sleepFn ?? sleep;
596
691
  const nowFn = dependencies.nowFn ?? Date.now;
597
692
  const featureCap = maxFeatures == null ? Number.POSITIVE_INFINITY : maxFeatures;
598
- const tileBboxes = bbox ? splitBbox(bbox, bboxTiles) : [undefined];
693
+ const tileBboxes = within || !bbox ? [undefined] : splitBbox(bbox, bboxTiles);
599
694
  const allFeatures = [];
600
695
  let pageCount = 0;
601
696
  let lastPage = null;
@@ -616,6 +711,7 @@ export class OSMFeatures {
616
711
  zoom,
617
712
  location,
618
713
  radius,
714
+ within,
619
715
  osmIds,
620
716
  minLengthM,
621
717
  maxLengthM,
@@ -639,7 +735,7 @@ export class OSMFeatures {
639
735
  while (tilePages < maxPages && allFeatures.length < featureCap) {
640
736
  let page;
641
737
  try {
642
- 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);
643
739
  if (raw.data instanceof ArrayBuffer) {
644
740
  throw appError(400, 'invalid_accept', 'query_all only supports GeoJSON; use query({ accept }) for binary encodings.');
645
741
  }
@@ -703,6 +799,31 @@ export class OSMFeatures {
703
799
  }
704
800
  return resultFromFeatures(features, meta);
705
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
+ }
706
827
  /** Preflight credit cost (``GET /v2/osm_features/cost``). */
707
828
  async estimate_cost(params = {}, dependencies = {}) {
708
829
  return this._getJson('/v2/osm_features/cost', buildFeaturesQuery({
@@ -716,6 +837,7 @@ export class OSMFeatures {
716
837
  zoom: params.zoom,
717
838
  location: params.location,
718
839
  radius: params.radius,
840
+ within: params.within,
719
841
  osmIds: params.osmIds,
720
842
  minLengthM: params.minLengthM,
721
843
  maxLengthM: params.maxLengthM,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osmfeatures",
3
- "version": "0.3.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",