@proximap/core 1.0.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.
@@ -0,0 +1,973 @@
1
+ /**
2
+ * Domain model for proximap. These types are provider-agnostic: OpenStreetMap
3
+ * is the default backend, but anything implementing {@link GeocodingProvider}
4
+ * and {@link PlacesProvider} can drive the same pipeline.
5
+ */
6
+ /** A WGS84 latitude/longitude coordinate, in decimal degrees. */
7
+ interface LatLng {
8
+ lat: number;
9
+ lng: number;
10
+ }
11
+ /**
12
+ * Normalized, top-level categories every POI is bucketed into. This is the
13
+ * single source of truth — {@link Category} is derived from it.
14
+ */
15
+ declare const CATEGORIES: readonly ["food", "grocery", "shopping", "healthcare", "education", "finance", "transport", "fuel", "parking", "accommodation", "leisure", "tourism", "worship", "public_service", "utility", "other"];
16
+ /** A normalized amenity/utility category. */
17
+ type Category = (typeof CATEGORIES)[number];
18
+ /**
19
+ * A single OSM tag selector, e.g. `{ key: 'amenity', value: 'cafe' }` or
20
+ * `{ key: 'cuisine', value: 'coffee_shop', regex: true }`. An omitted `value`
21
+ * matches the mere presence of the key.
22
+ */
23
+ interface CategorySelector {
24
+ key: string;
25
+ value?: string;
26
+ /** Treat `value` as an Overpass regular expression (the `~` operator). */
27
+ regex?: boolean;
28
+ }
29
+ /** A geocoded location — the resolved origin of a search. */
30
+ interface Place {
31
+ /** Best-effort short name, e.g. "Eiffel Tower". */
32
+ name: string;
33
+ /** Full human-readable label from the geocoder. */
34
+ displayName: string;
35
+ location: LatLng;
36
+ /** Geocoder-provided class/type, e.g. "tourism" or "city". */
37
+ kind?: string;
38
+ /** Bounding box as [south, north, west, east], if provided. */
39
+ boundingBox?: [number, number, number, number];
40
+ /** Identifier of the provider that produced this result, e.g. "nominatim". */
41
+ source: string;
42
+ /** Raw provider payload, for advanced consumers. */
43
+ raw?: unknown;
44
+ }
45
+ /** A point of interest: an amenity, utility, shop, or similar feature. */
46
+ interface Poi {
47
+ /** Stable identifier, e.g. "node/123" or "way/456". */
48
+ id: string;
49
+ name?: string;
50
+ category: Category;
51
+ /** The specific source value behind the category, e.g. "restaurant". */
52
+ kind?: string;
53
+ location: LatLng;
54
+ /** Original source tags/attributes (e.g. OSM tags). */
55
+ tags: Record<string, string>;
56
+ source: string;
57
+ /** Share of expected tags present for this category, in [0, 1]. */
58
+ completeness?: number;
59
+ /** Best-effort last-verified date (YYYY-MM-DD) from check_date/survey/meta. */
60
+ lastVerified?: string;
61
+ }
62
+ /** Whether a place is open at a given time: known states or "not enough data". */
63
+ type OpenState = 'open' | 'closed' | 'unknown';
64
+ /** A {@link Poi} enriched with distance from the search origin and a rank. */
65
+ interface RankedPoi extends Poi {
66
+ /** Great-circle distance from the origin, in metres. */
67
+ distanceMeters: number;
68
+ /** Composite score in [0, 1]; higher is better. */
69
+ score: number;
70
+ /** 1-based position after ranking. */
71
+ rank: number;
72
+ /** Open/closed/unknown at the queried time — set only when `open` was requested. */
73
+ openState?: OpenState;
74
+ /** ISO 8601 timestamp of the next open/closed transition, when computable. */
75
+ nextChange?: string;
76
+ /** Travel duration from the origin in seconds — set only when ranking by travel time. */
77
+ travelSeconds?: number;
78
+ /** Travel distance from the origin in metres — set only when ranking by travel time. */
79
+ travelMeters?: number;
80
+ /** A short human-readable reason for this rank — set only when `explain` is on. */
81
+ rankingReason?: string;
82
+ }
83
+ /** Options for a geocoding lookup. */
84
+ interface GeocodeOptions {
85
+ /** Maximum number of candidates to return. */
86
+ limit?: number;
87
+ /** Preferred result language, e.g. "en". */
88
+ language?: string;
89
+ signal?: AbortSignal;
90
+ }
91
+ /** Resolves place names/addresses to coordinates (and optionally back). */
92
+ interface GeocodingProvider {
93
+ readonly name: string;
94
+ geocode(query: string, options?: GeocodeOptions): Promise<Place[]>;
95
+ reverse?(location: LatLng, options?: GeocodeOptions): Promise<Place | null>;
96
+ }
97
+ /** Options for a nearby-places search. */
98
+ interface NearbyOptions {
99
+ /** Search radius from the center, in metres. */
100
+ radiusMeters: number;
101
+ /** Restrict results to these normalized categories (post-classification). */
102
+ categories?: Category[];
103
+ /**
104
+ * Tag selectors that drive a targeted query (and define what to keep). When
105
+ * set, the provider should fetch only matching features instead of the broad
106
+ * default set. Takes precedence over `categories` for query construction.
107
+ */
108
+ selectors?: CategorySelector[];
109
+ /** Upper bound on POIs fetched from the provider. */
110
+ limit?: number;
111
+ signal?: AbortSignal;
112
+ }
113
+ /** Finds points of interest around a coordinate. */
114
+ interface PlacesProvider {
115
+ readonly name: string;
116
+ findNearby(center: LatLng, options: NearbyOptions): Promise<Poi[]>;
117
+ }
118
+
119
+ /**
120
+ * Great-circle distance between two coordinates, in metres, via the haversine
121
+ * formula. Accurate to within ~0.5% — more than enough to rank nearby places.
122
+ */
123
+ declare function haversineMeters(a: LatLng, b: LatLng): number;
124
+ /** Format a metre distance as a compact human string ("125 m", "1.4 km"). */
125
+ declare function formatDistance(meters: number): string;
126
+ /** Format a second duration as a compact human string ("8 min", "1 h 5 min"). */
127
+ declare function formatDuration(seconds: number): string;
128
+ /**
129
+ * Parse a "lat,lng" string (decimal degrees) into a {@link LatLng}, or return
130
+ * null when the input is not a valid, in-range coordinate pair.
131
+ */
132
+ declare function parseCoordinates(input: string): LatLng | null;
133
+
134
+ interface Categorization {
135
+ category: Category;
136
+ /** The specific source value that drove the classification, if any. */
137
+ kind?: string;
138
+ }
139
+ /**
140
+ * Classify a set of OSM tags into a normalized {@link Category}. Keys are
141
+ * checked in priority order; a present-but-unknown key falls back to 'other'
142
+ * while preserving its value as `kind`.
143
+ */
144
+ declare function categorize(tags: Record<string, string>): Categorization;
145
+ /** Human-friendly display labels for each category. */
146
+ declare const CATEGORY_LABELS: Record<Category, string>;
147
+ /** Type guard: is `value` one of the known {@link Category} names? */
148
+ declare function isCategory(value: string): value is Category;
149
+
150
+ interface ResolvedCategories {
151
+ /** Deduplicated selectors for all recognized terms. */
152
+ selectors: CategorySelector[];
153
+ /** Recognized terms with their canonical name and category. */
154
+ matched: {
155
+ input: string;
156
+ term: string;
157
+ category: Category;
158
+ }[];
159
+ /** Terms that could not be resolved. */
160
+ unknown: string[];
161
+ }
162
+ /** Resolve a list of natural-language terms (or category names) to selectors. */
163
+ declare function resolveCategories(terms: readonly string[]): ResolvedCategories;
164
+ /** Is `term` a known category or synonym? */
165
+ declare function isKnownTerm(term: string): boolean;
166
+ /** All canonical query terms with their top-level category. */
167
+ declare function categoryVocabulary(): {
168
+ term: string;
169
+ category: Category;
170
+ }[];
171
+ /** Suggest known terms for an unrecognized input (typos, partial matches). */
172
+ declare function suggestCategories(term: string, limit?: number): string[];
173
+ /** Render a selector as an Overpass tag filter, e.g. `["amenity"="cafe"]`. */
174
+ declare function selectorToOverpassFilter(selector: CategorySelector): string;
175
+ /** Does a tag set satisfy a single selector? */
176
+ declare function tagsMatchSelector(tags: Record<string, string>, selector: CategorySelector): boolean;
177
+ /** Does a tag set satisfy any of the selectors? */
178
+ declare function tagsMatchAnySelector(tags: Record<string, string>, selectors: readonly CategorySelector[]): boolean;
179
+
180
+ /** Fraction of a category's expected tags that are present, in [0, 1]. */
181
+ declare function completenessOf(category: Category, tags: Record<string, string>): number;
182
+ /**
183
+ * Best-effort "last verified" date (YYYY-MM-DD): an explicit survey/check_date
184
+ * tag if present, else the element's last-edit timestamp.
185
+ */
186
+ declare function lastVerifiedOf(tags: Record<string, string>, timestamp?: string): string | undefined;
187
+ /**
188
+ * Collapse duplicate representations of the same POI (e.g. a node and a building
189
+ * way) into one, keeping the richer entry. Order of first appearance is kept.
190
+ */
191
+ declare function dedupePois(pois: Poi[]): Poi[];
192
+
193
+ interface OpeningEvaluation {
194
+ state: OpenState;
195
+ /** ISO 8601 timestamp of the next open/closed transition, when computable. */
196
+ nextChange?: string;
197
+ }
198
+ /**
199
+ * A small, dependency-free evaluator for the common subset of the OSM
200
+ * `opening_hours` grammar: weekday ranges/lists, multiple time ranges,
201
+ * overnight (midnight-wrapping) ranges, `24/7`, and `off`/`closed`. Public- and
202
+ * school-holiday rules (`PH`/`SH`) are skipped (no holiday calendar), so a
203
+ * normal day still evaluates from the regular rules.
204
+ *
205
+ * Anything outside this subset — `sunrise`/`sunset`, month/date/week selectors,
206
+ * open-ended `08:00+`, etc. — yields `unknown` rather than a guess. A missing or
207
+ * empty value is `unknown` too. We never assert a state we can't justify.
208
+ *
209
+ * Times are read from the local-time fields of `when`; to evaluate a POI in
210
+ * another timezone, pass a `when` already shifted into that zone.
211
+ */
212
+ declare function isOpenAt(openingHours: string | undefined, when: Date): OpeningEvaluation;
213
+
214
+ /**
215
+ * Minimal JSON-over-HTTP helper built on the platform `fetch`. Keeps the core
216
+ * dependency-free while handling timeouts, cancellation, retries with backoff,
217
+ * optional caching, and consistent error surfacing across providers.
218
+ */
219
+ /** Default contact identifier sent to OSM services; override per provider. */
220
+ declare const DEFAULT_USER_AGENT = "proximap/0.1 (+https://github.com/AmeyaBorkar/proximap)";
221
+ /** A pluggable response cache. Values are parsed JSON. */
222
+ interface RequestCache {
223
+ get(key: string): Promise<unknown> | unknown;
224
+ set(key: string, value: unknown): Promise<void> | void;
225
+ }
226
+ /** A process-local, unbounded cache. Opt-in — pass it to a provider to enable. */
227
+ declare class InMemoryCache implements RequestCache {
228
+ private readonly store;
229
+ get(key: string): unknown;
230
+ set(key: string, value: unknown): void;
231
+ }
232
+ /**
233
+ * Serializes calls so that consecutive `acquire()`s are spaced at least
234
+ * `minIntervalMs` apart — for honouring usage policies (e.g. Nominatim's 1 req/s).
235
+ */
236
+ declare class RateLimiter {
237
+ private readonly minIntervalMs;
238
+ private last;
239
+ private chain;
240
+ constructor(minIntervalMs: number);
241
+ acquire(): Promise<void>;
242
+ }
243
+ interface RequestOptions {
244
+ method?: 'GET' | 'POST';
245
+ headers?: Record<string, string>;
246
+ body?: string;
247
+ /** External cancellation signal; merged with the internal timeout. */
248
+ signal?: AbortSignal;
249
+ /** Abort the request after this many milliseconds (default 20000). */
250
+ timeoutMs?: number;
251
+ /** Retry attempts on transient failures (429/5xx/timeout/network). Default 0. */
252
+ retries?: number;
253
+ /** Base backoff delay in ms, doubled each attempt (default 500). */
254
+ retryDelayMs?: number;
255
+ /** Optional response cache, keyed by method + url + body. */
256
+ cache?: RequestCache;
257
+ }
258
+ /** Thrown when a request times out or returns a non-2xx response. */
259
+ declare class HttpError extends Error {
260
+ readonly status: number;
261
+ readonly url: string;
262
+ constructor(status: number, url: string, message: string);
263
+ }
264
+ /** Perform an HTTP request and parse the JSON body as `T`, with retry + cache. */
265
+ declare function requestJson<T>(url: string, options?: RequestOptions): Promise<T>;
266
+
267
+ interface NominatimOptions {
268
+ /** Base URL of the Nominatim instance (no trailing slash required). */
269
+ endpoint?: string;
270
+ /** Contact User-Agent, required by the public OSM instance's usage policy. */
271
+ userAgent?: string;
272
+ /** Per-request timeout in milliseconds. */
273
+ timeoutMs?: number;
274
+ /** Minimum spacing between requests in ms (default 1000, per OSM policy). */
275
+ minIntervalMs?: number;
276
+ /** Retry attempts on transient failures (default 2). */
277
+ retries?: number;
278
+ /** Optional response cache (opt-in). */
279
+ cache?: RequestCache;
280
+ }
281
+ /**
282
+ * Geocoding via Nominatim (OpenStreetMap). Free and key-less; please honour the
283
+ * usage policy (max ~1 req/s, valid User-Agent) or point `endpoint` at your own
284
+ * instance for production traffic.
285
+ */
286
+ declare class NominatimGeocoder implements GeocodingProvider {
287
+ readonly name = "nominatim";
288
+ private readonly endpoint;
289
+ private readonly userAgent;
290
+ private readonly timeoutMs;
291
+ private readonly retries;
292
+ private readonly cache;
293
+ private readonly limiter;
294
+ constructor(options?: NominatimOptions);
295
+ geocode(query: string, options?: GeocodeOptions): Promise<Place[]>;
296
+ reverse(location: LatLng, options?: GeocodeOptions): Promise<Place | null>;
297
+ private request;
298
+ private toPlace;
299
+ }
300
+
301
+ interface OverpassOptions {
302
+ /** Overpass interpreter endpoint URL. */
303
+ endpoint?: string;
304
+ /** Contact User-Agent sent with each request. */
305
+ userAgent?: string;
306
+ /** Per-request timeout in milliseconds. */
307
+ timeoutMs?: number;
308
+ /** Minimum spacing between requests in ms (default 1000). */
309
+ minIntervalMs?: number;
310
+ /** Retry attempts on transient failures (default 2). */
311
+ retries?: number;
312
+ /** Optional response cache (opt-in). */
313
+ cache?: RequestCache;
314
+ }
315
+ /** Build the Overpass QL query for all relevant POIs within `radiusMeters`. */
316
+ declare function buildOverpassQuery(center: LatLng, radiusMeters: number): string;
317
+ /** Build an Overpass query that fetches only features matching `selectors`. */
318
+ declare function buildTargetedOverpassQuery(center: LatLng, radiusMeters: number, selectors: CategorySelector[]): string;
319
+ /**
320
+ * Nearby-places search via the Overpass API over OpenStreetMap data. Fetches
321
+ * every relevant feature within the radius, classifies it, and (optionally)
322
+ * filters by category — distance ranking happens upstream.
323
+ */
324
+ declare class OverpassPlacesProvider implements PlacesProvider {
325
+ readonly name = "overpass";
326
+ private readonly endpoint;
327
+ private readonly userAgent;
328
+ private readonly timeoutMs;
329
+ private readonly retries;
330
+ private readonly cache;
331
+ private readonly limiter;
332
+ constructor(options?: OverpassOptions);
333
+ findNearby(center: LatLng, options: NearbyOptions): Promise<Poi[]>;
334
+ private toPoi;
335
+ }
336
+
337
+ /** How a route is travelled. Maps to engine-specific profiles per adapter. */
338
+ type TravelMode = 'walk' | 'bike' | 'drive';
339
+ interface RouteMetric {
340
+ /** Travel duration in seconds. */
341
+ seconds: number;
342
+ /** Travel distance in metres. */
343
+ meters: number;
344
+ }
345
+ /** A polygon ring as [longitude, latitude] pairs (GeoJSON order). */
346
+ type PolygonRing = [number, number][];
347
+ interface RoutingRequestOptions {
348
+ signal?: AbortSignal;
349
+ }
350
+ /**
351
+ * Routing is a commodity (OSRM/Valhalla/ORS self-host it); proximap's value is
352
+ * *composing* it with the amenity layer. This is the seam: a one-to-many matrix
353
+ * and an optional isochrone, with adapters for real engines and a haversine
354
+ * fallback so everything degrades gracefully and works key-free out of the box.
355
+ */
356
+ interface RoutingProvider {
357
+ readonly name: string;
358
+ /** Durations/distances from one origin to many targets, aligned with `targets` (null = unreachable). */
359
+ matrix(origin: LatLng, targets: readonly LatLng[], mode: TravelMode, options?: RoutingRequestOptions): Promise<(RouteMetric | null)[]>;
360
+ /** Polygon reachable within `minutes`. Optional — absent ⇒ callers fall back to a matrix threshold. */
361
+ isochrone?(origin: LatLng, minutes: number, mode: TravelMode, options?: RoutingRequestOptions): Promise<PolygonRing>;
362
+ }
363
+ /** Typical speeds (m/s) for the haversine fallback: ~5, ~15, ~40 km/h. */
364
+ declare const MODE_SPEED_MPS: Record<TravelMode, number>;
365
+ /**
366
+ * Straight-line routing: distance via haversine, duration via a per-mode speed,
367
+ * isochrone as a circle. The key-free, network-free default — a floor that always
368
+ * works; pass a real {@link RoutingProvider} (Valhalla/OSRM) for road accuracy.
369
+ */
370
+ declare class HaversineRoutingProvider implements RoutingProvider {
371
+ readonly name = "haversine";
372
+ matrix(origin: LatLng, targets: readonly LatLng[], mode: TravelMode): Promise<RouteMetric[]>;
373
+ isochrone(origin: LatLng, minutes: number, mode: TravelMode): Promise<PolygonRing>;
374
+ }
375
+ /** Approximate a circle of `radiusMeters` around `center` as a polygon ring ([lng, lat]). */
376
+ declare function circlePolygon(center: LatLng, radiusMeters: number, steps?: number): PolygonRing;
377
+ /** Ray-casting point-in-polygon test; `ring` is [lng, lat] pairs. */
378
+ declare function pointInPolygon(point: LatLng, ring: PolygonRing): boolean;
379
+
380
+ interface ValhallaOptions {
381
+ /** Base URL of the Valhalla instance (default: the key-free FOSSGIS instance). */
382
+ endpoint?: string;
383
+ userAgent?: string;
384
+ timeoutMs?: number;
385
+ /** Minimum spacing between requests in ms (default 1000). */
386
+ minIntervalMs?: number;
387
+ retries?: number;
388
+ cache?: RequestCache;
389
+ }
390
+ /**
391
+ * Routing via Valhalla. Defaults to the **key-free** public FOSSGIS instance,
392
+ * which supports pedestrian/bicycle/auto matrices and real isochrone polygons —
393
+ * please honour its ~1 req/s fair-use policy or self-host for volume.
394
+ */
395
+ declare class ValhallaRoutingProvider implements RoutingProvider {
396
+ readonly name = "valhalla";
397
+ private readonly endpoint;
398
+ private readonly userAgent;
399
+ private readonly timeoutMs;
400
+ private readonly retries;
401
+ private readonly cache;
402
+ private readonly limiter;
403
+ constructor(options?: ValhallaOptions);
404
+ matrix(origin: LatLng, targets: readonly LatLng[], mode: TravelMode, options?: RoutingRequestOptions): Promise<(RouteMetric | null)[]>;
405
+ isochrone(origin: LatLng, minutes: number, mode: TravelMode, options?: RoutingRequestOptions): Promise<PolygonRing>;
406
+ private post;
407
+ }
408
+
409
+ interface OsrmOptions {
410
+ /** Base URL of the OSRM instance (default: the public demo server). */
411
+ endpoint?: string;
412
+ userAgent?: string;
413
+ timeoutMs?: number;
414
+ minIntervalMs?: number;
415
+ retries?: number;
416
+ cache?: RequestCache;
417
+ }
418
+ /**
419
+ * Travel-time matrices via OSRM's Table service. The public demo server
420
+ * (`router.project-osrm.org`) only offers the car profile; for walk/bike,
421
+ * point `endpoint` at a self-hosted OSRM with the matching profile. Matrix only —
422
+ * OSRM has no isochrone service (use {@link ValhallaRoutingProvider} for that).
423
+ */
424
+ declare class OsrmRoutingProvider implements RoutingProvider {
425
+ readonly name = "osrm";
426
+ private readonly endpoint;
427
+ private readonly userAgent;
428
+ private readonly timeoutMs;
429
+ private readonly retries;
430
+ private readonly cache;
431
+ private readonly limiter;
432
+ constructor(options?: OsrmOptions);
433
+ matrix(origin: LatLng, targets: readonly LatLng[], mode: TravelMode, options?: RoutingRequestOptions): Promise<(RouteMetric | null)[]>;
434
+ }
435
+
436
+ /** Inputs handed to a scorer for a single POI. */
437
+ interface ScoreInput {
438
+ poi: Poi;
439
+ distanceMeters: number;
440
+ /** Radius used to normalize distance into a [0, 1] proximity. */
441
+ radiusMeters: number;
442
+ }
443
+ interface RankOptions {
444
+ /** Per-category multipliers applied to the base score (default 1 each). */
445
+ categoryWeights?: Partial<Record<Category, number>>;
446
+ /** Custom scorer (higher = better); overrides the default proximity scorer. */
447
+ scoreFn?: (input: ScoreInput) => number;
448
+ /** Distance normalization radius; defaults to the farthest POI found. */
449
+ radiusMeters?: number;
450
+ }
451
+ /**
452
+ * Rank POIs by proximity to `origin`. By default results are ordered nearest
453
+ * first; supplying `categoryWeights` or a custom `scoreFn` switches to
454
+ * highest-score first (ties broken by distance). Each result gains its
455
+ * great-circle `distanceMeters`, a `score` in [0, 1], and a 1-based `rank`.
456
+ */
457
+ declare function rankByProximity(origin: LatLng, pois: Poi[], options?: RankOptions): RankedPoi[];
458
+
459
+ /**
460
+ * Composable consumer/accessibility facets that OSM tags carry but most apps
461
+ * never expose as combinable filters: dietary options, cuisine, payment
462
+ * methods, connectivity, seating, and step-free access. Each facet compiles to
463
+ * a predicate over a POI's tags; a POI must satisfy *all* active facets.
464
+ *
465
+ * Sparsity note: a missing tag is treated as "unknown", which means the POI is
466
+ * not a positive match — never that it "fails". We don't claim a place lacks a
467
+ * feature, only that OSM doesn't record it.
468
+ */
469
+ interface FacetFilters {
470
+ /** Dietary options, e.g. "vegan", "vegetarian", "halal" (matches diet:<x>=yes|only). */
471
+ diet?: string | string[];
472
+ /** Cuisine tokens, e.g. "italian", "pizza" (matches the ;-separated cuisine tag). */
473
+ cuisine?: string | string[];
474
+ /** Accepted payments, e.g. "contactless", "cards", "visa" (matches payment:<x>). */
475
+ payment?: string | string[];
476
+ /** Require internet access (internet_access present and not "no"). */
477
+ internetAccess?: boolean;
478
+ /** Require outdoor seating. */
479
+ outdoorSeating?: boolean;
480
+ /** Require takeaway (takeaway=yes|only). */
481
+ takeaway?: boolean;
482
+ /** Require delivery. */
483
+ delivery?: boolean;
484
+ /** Required wheelchair value(s), e.g. "yes" or ["yes", "limited"]. */
485
+ wheelchair?: string | string[];
486
+ /** Raw tag constraints: `true` = present, `false` = absent, string = equals. */
487
+ tags?: Record<string, string | boolean>;
488
+ }
489
+ /** A compiled facet check over a POI's tag set. */
490
+ type FacetPredicate = (tags: Record<string, string>) => boolean;
491
+ /** Compile a {@link FacetFilters} object into a list of tag predicates (AND-ed). */
492
+ declare function compileFacets(filters: FacetFilters): FacetPredicate[];
493
+ /** Does a tag set satisfy every compiled facet predicate? */
494
+ declare function matchesFacets(tags: Record<string, string>, predicates: readonly FacetPredicate[]): boolean;
495
+ /**
496
+ * A ranking scorer for accessibility-first search: step-free (`wheelchair=yes`)
497
+ * POIs rank above `limited`, which rank above unknown/none — with distance
498
+ * breaking ties *within* each tier. Tiers occupy non-overlapping score bands so
499
+ * an accessible-but-slightly-farther place still outranks a closer inaccessible
500
+ * one, as the use case demands.
501
+ */
502
+ declare function accessibleScorer(): (input: ScoreInput) => number;
503
+
504
+ interface ResolveOriginOptions {
505
+ language?: string;
506
+ signal?: AbortSignal;
507
+ }
508
+ /**
509
+ * Resolve a place name, a "lat,lng" string, or a {@link LatLng} into an origin
510
+ * {@link Place}. Coordinate inputs are enriched with a best-effort reverse
511
+ * geocode when the provider supports it, falling back to the raw coordinates.
512
+ */
513
+ declare function resolveOrigin(query: string | LatLng, geocoder: GeocodingProvider, options?: ResolveOriginOptions): Promise<Place>;
514
+
515
+ interface DisambiguateOptions {
516
+ geocoder?: GeocodingProvider;
517
+ /** How many candidates to fetch/return (default 5). */
518
+ limit?: number;
519
+ language?: string;
520
+ signal?: AbortSignal;
521
+ }
522
+ interface Disambiguation {
523
+ query: string;
524
+ /**
525
+ * True when several plausible, geographically distinct candidates exist (e.g.
526
+ * the ~90 US "Springfield"s). Callers should present `candidates` rather than
527
+ * silently trusting `best`, since geocoder relevance is not correctness.
528
+ */
529
+ ambiguous: boolean;
530
+ /** The top-ranked candidate (the geocoder's best guess), or null if none. */
531
+ best: Place | null;
532
+ /** Ranked candidates to disambiguate between. */
533
+ candidates: Place[];
534
+ }
535
+ /**
536
+ * Geocode a query and decide whether it is ambiguous — multiple distinct places
537
+ * a human would need to choose between — instead of silently taking result #1.
538
+ * This is the agent-safety guard against confidently-wrong locations.
539
+ */
540
+ declare function disambiguateLocation(query: string, options?: DisambiguateOptions): Promise<Disambiguation>;
541
+
542
+ /** The closest POI matching a selector set, with its distance from an origin. */
543
+ interface NearestMatch {
544
+ /** Distance to the nearest match in metres, or null if none matched. */
545
+ meters: number | null;
546
+ /** The nearest matching POI, or null if none matched. */
547
+ poi: Poi | null;
548
+ }
549
+ /**
550
+ * Find the nearest POI to `origin` whose tags satisfy any of `selectors`.
551
+ * Shared by the gap and walkability features; returns nulls (never throws)
552
+ * when nothing matches, so callers can frame absence honestly.
553
+ */
554
+ declare function nearestMatchingPoi(origin: LatLng, pois: readonly Poi[], selectors: readonly CategorySelector[]): NearestMatch;
555
+
556
+ interface FindNearbyOptions {
557
+ /** Search radius in metres (default 1000). */
558
+ radiusMeters?: number;
559
+ /**
560
+ * Restrict to these categories. Accepts the 16 normalized category names and
561
+ * natural-language terms ("coffee", "pharmacy", "petrol"). Unknown terms throw
562
+ * with suggestions.
563
+ */
564
+ categories?: Array<Category | (string & {})>;
565
+ /** Max results to return after ranking (default 30; <= 0 means no limit). */
566
+ limit?: number;
567
+ /** Preferred language for geocoding results. */
568
+ language?: string;
569
+ /** Override the geocoder (default: Nominatim/OSM). */
570
+ geocoder?: GeocodingProvider;
571
+ /** Override the places provider (default: Overpass/OSM). */
572
+ places?: PlacesProvider;
573
+ /**
574
+ * Composable consumer/accessibility facets (diet, payment, wifi, wheelchair…).
575
+ * A POI must satisfy every active facet; a missing tag means "not a match",
576
+ * never asserted as the feature being absent.
577
+ */
578
+ filters?: FacetFilters;
579
+ /**
580
+ * Accessibility-first ranking: step-free POIs rank above `limited`, above
581
+ * unknown/none, with distance breaking ties within each tier. Ignored when a
582
+ * custom `rank.scoreFn` is supplied.
583
+ */
584
+ accessible?: boolean;
585
+ /**
586
+ * Keep only places open at this time and annotate each result with
587
+ * `openState`/`nextChange`. `'now'` uses the current time; `{ at }` takes an
588
+ * ISO string or Date. Places whose hours are unknown are kept and labelled
589
+ * `unknown` (never silently dropped); only confirmed-closed places are
590
+ * removed. Times are read as the POI's local wall-clock (see {@link isOpenAt}).
591
+ */
592
+ open?: 'now' | {
593
+ at: string | Date;
594
+ };
595
+ /**
596
+ * Order results by straight-line `'distance'` (default) or by `'travelTime'`.
597
+ * Travel-time ranking attaches `travelSeconds`/`travelMeters` to each result.
598
+ */
599
+ rankBy?: 'distance' | 'travelTime';
600
+ /** Travel mode for `rankBy: 'travelTime'` (default `'walk'`). */
601
+ mode?: TravelMode;
602
+ /**
603
+ * Routing engine for travel-time ranking (default: {@link HaversineRoutingProvider},
604
+ * key-free straight-line estimates). Pass a real engine for road-network times;
605
+ * if it errors, ranking falls back to haversine and `result.routing.fellBack` is set.
606
+ */
607
+ routing?: RoutingProvider;
608
+ /** Attach a short `rankingReason` to each result (e.g. "closest open cafe, 240 m"). */
609
+ explain?: boolean;
610
+ /** Ranking tweaks (category weights or a custom scorer). */
611
+ rank?: RankOptions;
612
+ signal?: AbortSignal;
613
+ }
614
+ interface NearbyResult {
615
+ origin: Place;
616
+ results: RankedPoi[];
617
+ /** Number of POIs found before `limit` was applied. */
618
+ total: number;
619
+ /** Set when `rankBy: 'travelTime'` was used: which engine answered, and the mode. */
620
+ routing?: {
621
+ provider: string;
622
+ mode: TravelMode;
623
+ fellBack: boolean;
624
+ };
625
+ }
626
+ /**
627
+ * Resolve a place name or coordinate to an origin, find surrounding amenities,
628
+ * and rank them by distance. This is the headline entry point of proximap.
629
+ *
630
+ * @param query A place name/address, a "lat,lng" string, or a {@link LatLng}.
631
+ */
632
+ declare function findNearbyAmenities(query: string | LatLng, options?: FindNearbyOptions): Promise<NearbyResult>;
633
+
634
+ interface ReachableOptions {
635
+ /** Time budget in minutes. */
636
+ within: number;
637
+ /** Travel mode (default `'walk'`). */
638
+ mode?: TravelMode;
639
+ /** Restrict to these categories/terms (default: all amenities). */
640
+ categories?: Array<Category | (string & {})>;
641
+ /**
642
+ * Routing engine (default {@link HaversineRoutingProvider}). A provider with an
643
+ * `isochrone` method gives a true reachability polygon; otherwise membership is
644
+ * decided by a travel-time matrix threshold.
645
+ */
646
+ routing?: RoutingProvider;
647
+ geocoder?: GeocodingProvider;
648
+ places?: PlacesProvider;
649
+ language?: string;
650
+ signal?: AbortSignal;
651
+ }
652
+ interface ReachableResult {
653
+ origin: Place;
654
+ withinMinutes: number;
655
+ mode: TravelMode;
656
+ /** The isochrone polygon used ([lng, lat] ring), or null if none was available. */
657
+ isochrone: PolygonRing | null;
658
+ /** Amenities reachable within the budget, soonest first. */
659
+ results: RankedPoi[];
660
+ count: number;
661
+ }
662
+ /**
663
+ * Return the amenities reachable within a time budget — the *answer*, not just a
664
+ * polygon. With an isochrone-capable engine, membership is the true road-network
665
+ * polygon (point-in-polygon); otherwise it falls back to a travel-time matrix
666
+ * threshold. Results are annotated with travel time and sorted soonest-first.
667
+ */
668
+ declare function reachableAmenities(query: string | LatLng, options: ReachableOptions): Promise<ReachableResult>;
669
+
670
+ /** Everyday needs a well-served neighbourhood should provide nearby. */
671
+ declare const DEFAULT_DAILY_NEEDS: readonly ["grocery", "pharmacy", "healthcare", "food", "finance", "transport", "education", "park"];
672
+ interface GapOptions {
673
+ /** Category terms to check (default: {@link DEFAULT_DAILY_NEEDS}). */
674
+ categories?: string[];
675
+ /** How far to look for the nearest instance, in metres (default 5000). */
676
+ searchRadiusMeters?: number;
677
+ /** Distance beyond which a category counts as a gap, in metres (default 1500). */
678
+ thresholdMeters?: number;
679
+ geocoder?: GeocodingProvider;
680
+ places?: PlacesProvider;
681
+ language?: string;
682
+ signal?: AbortSignal;
683
+ }
684
+ interface CategoryGap {
685
+ /** The requested category term. */
686
+ category: string;
687
+ /** Distance to the nearest match, or null if none was found within the search radius. */
688
+ nearestMeters: number | null;
689
+ isGap: boolean;
690
+ /** Data-completeness of the nearest match (a confidence hint), when known. */
691
+ nearestCompleteness?: number;
692
+ }
693
+ interface GapReport {
694
+ origin: Place;
695
+ searchRadiusMeters: number;
696
+ thresholdMeters: number;
697
+ /** Every requested category with its nearest-match status. */
698
+ gaps: CategoryGap[];
699
+ /** Categories flagged as gaps — i.e. not found in OSM within the threshold. */
700
+ missing: string[];
701
+ }
702
+ /**
703
+ * Report which everyday amenities are missing or far from a location — the
704
+ * inverse of "what's nearby". Because OSM under-maps some areas, absence is
705
+ * framed as "not found in OSM within the threshold", never asserted as truth;
706
+ * `nearestCompleteness` hints at data confidence.
707
+ */
708
+ declare function detectGaps(query: string | LatLng, options?: GapOptions): Promise<GapReport>;
709
+
710
+ /**
711
+ * A daily-need category and how much it counts toward the walkability score.
712
+ * Weights are relative — the score normalizes by their sum.
713
+ */
714
+ interface CategoryWeight {
715
+ /** A natural-language category term (resolved via the taxonomy). */
716
+ term: string;
717
+ weight: number;
718
+ }
719
+ /**
720
+ * Default basket of daily needs and weights, loosely modelled on the published
721
+ * Walk Score categories but fully open and tunable. Groceries, food, pharmacy,
722
+ * and transit count most; civic/leisure round out a complete neighbourhood.
723
+ */
724
+ declare const DEFAULT_WALK_CATEGORIES: CategoryWeight[];
725
+ interface WalkabilityDecay {
726
+ /** At/below this distance (m) a category scores full marks (default 400 ≈ 5-min walk). */
727
+ idealMeters?: number;
728
+ /** At/beyond this distance (m) a category scores zero (default 2400 ≈ 30-min walk). */
729
+ maxMeters?: number;
730
+ }
731
+ interface WalkabilityOptions {
732
+ /** Daily-need categories and weights (default {@link DEFAULT_WALK_CATEGORIES}). */
733
+ categories?: CategoryWeight[];
734
+ /** Distance-decay tuning (defaults: full credit ≤ 400 m, zero ≥ 2400 m). */
735
+ decay?: WalkabilityDecay;
736
+ /** How far to search for the nearest of each category (default = decay max). */
737
+ searchRadiusMeters?: number;
738
+ geocoder?: GeocodingProvider;
739
+ places?: PlacesProvider;
740
+ language?: string;
741
+ signal?: AbortSignal;
742
+ }
743
+ interface CategoryScore {
744
+ category: string;
745
+ weight: number;
746
+ /** Distance to the nearest match, or null if none found within the search radius. */
747
+ nearestMeters: number | null;
748
+ /** Distance-decay sub-score in [0, 1]. */
749
+ subScore: number;
750
+ /** Data-completeness of the nearest match (a confidence hint), when known. */
751
+ nearestCompleteness?: number;
752
+ }
753
+ interface WalkabilityReport {
754
+ origin: Place;
755
+ /** Overall walkability in [0, 100]; higher is more walkable. */
756
+ score: number;
757
+ /**
758
+ * Confidence in [0, 1] reflecting OSM data density around the origin — low
759
+ * where few categories were found or their tagging is sparse. A low score
760
+ * with low confidence means "thin data here", not "nothing here".
761
+ */
762
+ confidence: number;
763
+ /** Per-category nearest distance and sub-score. */
764
+ breakdown: CategoryScore[];
765
+ /** Categories with no match found within the search radius. */
766
+ missing: string[];
767
+ /** The distance-decay bounds actually used. */
768
+ decay: {
769
+ idealMeters: number;
770
+ maxMeters: number;
771
+ };
772
+ }
773
+ /**
774
+ * Distance-decay sub-score: full credit within `ideal`, linearly declining to
775
+ * zero at `max`. Deliberately simple and transparent so the score is auditable
776
+ * and tunable, unlike opaque proprietary indices.
777
+ */
778
+ declare function walkSubScore(meters: number | null, idealMeters: number, maxMeters: number): number;
779
+ /**
780
+ * Score how walkable / well-served a location is: a 0–100 number plus a full
781
+ * per-category breakdown, the categories that are missing, and a data-confidence
782
+ * note. An open, transparent, tunable, OSM-native alternative to proprietary
783
+ * walkability indices — every input is visible and adjustable.
784
+ */
785
+ declare function walkabilityScore(query: string | LatLng, options?: WalkabilityOptions): Promise<WalkabilityReport>;
786
+
787
+ interface CompareOptions {
788
+ /** Dimensions and weights to compare on (default: the walkability basket). */
789
+ categories?: CategoryWeight[];
790
+ /** Distance-decay tuning, passed through to each location's scoring. */
791
+ decay?: WalkabilityDecay;
792
+ /** How far to search around each location (default = decay max). */
793
+ searchRadiusMeters?: number;
794
+ geocoder?: GeocodingProvider;
795
+ places?: PlacesProvider;
796
+ language?: string;
797
+ signal?: AbortSignal;
798
+ }
799
+ interface LocationScore {
800
+ origin: Place;
801
+ /** Walkability score (0–100) under the comparison weights. */
802
+ score: number;
803
+ confidence: number;
804
+ breakdown: CategoryScore[];
805
+ missing: string[];
806
+ }
807
+ interface DimensionWinner {
808
+ category: string;
809
+ weight: number;
810
+ /** Index into `locations` of the best location for this dimension, or null if none has it. */
811
+ bestIndex: number | null;
812
+ }
813
+ interface RankedLocation {
814
+ /** Index into `locations` (input order). */
815
+ index: number;
816
+ score: number;
817
+ origin: Place;
818
+ }
819
+ interface ComparisonReport {
820
+ /** Each location's score, in input order. */
821
+ locations: LocationScore[];
822
+ /** Locations sorted best-first (ties: higher confidence, then input order). */
823
+ ranked: RankedLocation[];
824
+ /** The top-ranked location, or null if no candidates were given. */
825
+ best: RankedLocation | null;
826
+ /** For each dimension, which location is best served. */
827
+ dimensions: DimensionWinner[];
828
+ /** The weights actually used. */
829
+ weights: CategoryWeight[];
830
+ }
831
+ /**
832
+ * Compare N candidate locations across weighted daily-need dimensions and rank
833
+ * them — a key-free, arbitrary-N, OSM-native relocation/siting scorecard. Pure
834
+ * composition over {@link walkabilityScore}: each location is scored the same
835
+ * way, then ranked and compared dimension-by-dimension.
836
+ *
837
+ * Out of scope by design (not in OSM): transit *frequency*, school quality,
838
+ * crime, prices. We compare amenity *access*, and carry through walkability's
839
+ * confidence so thin data reads as low confidence, not a confident zero.
840
+ */
841
+ declare function compareLocations(queries: ReadonlyArray<string | LatLng>, options?: CompareOptions): Promise<ComparisonReport>;
842
+
843
+ interface ErrandOptions {
844
+ /** Categories/terms to hit one of each, e.g. ['pharmacy', 'atm', 'grocery']. */
845
+ categories: Array<Category | (string & {})>;
846
+ /** Travel mode for the cost matrix (default `'walk'`). */
847
+ mode?: TravelMode;
848
+ /** Optional fixed end point (a place name, "lat,lng", or coordinate). */
849
+ end?: string | LatLng;
850
+ /** Nearest candidates considered per category (default 5). */
851
+ candidatesPerCategory?: number;
852
+ /** How far to look for candidates, in metres (default 3000). */
853
+ searchRadiusMeters?: number;
854
+ /**
855
+ * Cost engine for the matrix (default {@link HaversineRoutingProvider} — an
856
+ * honest, instant, key-free straight-line MVP). Pass a real engine for road
857
+ * times; note it issues one matrix request per point.
858
+ */
859
+ routing?: RoutingProvider;
860
+ geocoder?: GeocodingProvider;
861
+ places?: PlacesProvider;
862
+ language?: string;
863
+ signal?: AbortSignal;
864
+ }
865
+ interface ErrandStop {
866
+ category: string;
867
+ poi: Poi;
868
+ /** Leg from the previous point (origin or prior stop) to this stop. */
869
+ legSeconds: number;
870
+ legMeters: number;
871
+ }
872
+ interface ErrandPlan {
873
+ origin: Place;
874
+ end: Place | null;
875
+ mode: TravelMode;
876
+ /** Chosen places in visit order. */
877
+ stops: ErrandStop[];
878
+ totalSeconds: number;
879
+ totalMeters: number;
880
+ /** Requested categories with no candidate nearby — skipped, not faked. */
881
+ missing: string[];
882
+ candidatesPerCategory: number;
883
+ }
884
+ /**
885
+ * Plan the shortest trip that buys/visits one of each requested category near an
886
+ * origin — the **Generalized TSP** ("pick one per set, then optimize") that no
887
+ * consumer app ships. Fetches the nearest candidates per category, builds a cost
888
+ * matrix, and solves exactly via a grouped Held-Karp DP (instant at consumer
889
+ * scale). Categories with no candidate are reported as `missing`, never faked.
890
+ */
891
+ declare function planErrands(query: string | LatLng, options: ErrandOptions): Promise<ErrandPlan>;
892
+
893
+ /**
894
+ * ODbL attribution for exported OpenStreetMap data. OSM's licence lets you store
895
+ * and redistribute results (unlike the commercial APIs) **provided you keep this
896
+ * notice** — so exporters emit it and callers should keep it with the data.
897
+ */
898
+ declare const ODBL_ATTRIBUTION = "\u00A9 OpenStreetMap contributors, ODbL (https://www.openstreetmap.org/copyright)";
899
+ interface GeoJsonFeature {
900
+ type: 'Feature';
901
+ /** GeoJSON uses [longitude, latitude] order (RFC 7946). */
902
+ geometry: {
903
+ type: 'Point';
904
+ coordinates: [number, number];
905
+ };
906
+ properties: Record<string, unknown>;
907
+ }
908
+ interface GeoJsonFeatureCollection {
909
+ type: 'FeatureCollection';
910
+ /** ODbL attribution, per {@link ODBL_ATTRIBUTION}. */
911
+ attribution: string;
912
+ features: GeoJsonFeature[];
913
+ }
914
+ /** Serialize a nearby-search result to a GeoJSON FeatureCollection (one Point per POI). */
915
+ declare function toGeoJSON(result: NearbyResult): GeoJsonFeatureCollection;
916
+ /** Serialize a nearby-search result to RFC 4180 CSV (header row + one row per POI). */
917
+ declare function toCSV(result: NearbyResult): string;
918
+
919
+ /**
920
+ * A stored snapshot of an area's POIs. OSM data (ODbL) may be freely stored and
921
+ * redistributed — the advantage commercial APIs forbid — so this is a portable,
922
+ * offline-queryable dataset. Keep the `attribution` with the data.
923
+ */
924
+ interface SnapshotDataset {
925
+ attribution: string;
926
+ /** ISO timestamp the snapshot was captured. */
927
+ createdAt: string;
928
+ /** Center the snapshot was taken around. */
929
+ center: LatLng;
930
+ /** Radius captured, in metres. */
931
+ radiusMeters: number;
932
+ /** Normalized, deduplicated POIs in the area. */
933
+ pois: Poi[];
934
+ }
935
+ interface SnapshotOptions {
936
+ /** Radius to capture, in metres (default 2000). */
937
+ radiusMeters?: number;
938
+ /** Restrict the capture to these categories/terms (default: all amenities). */
939
+ categories?: Array<Category | (string & {})>;
940
+ geocoder?: GeocodingProvider;
941
+ places?: PlacesProvider;
942
+ language?: string;
943
+ signal?: AbortSignal;
944
+ /** Override the capture timestamp (ISO) — for deterministic output/tests. */
945
+ createdAt?: string;
946
+ }
947
+ /**
948
+ * Capture an area's POIs into a {@link SnapshotDataset} for offline reuse. Pair
949
+ * with {@link DatasetPlacesProvider} to answer queries with no network calls.
950
+ */
951
+ declare function snapshotArea(query: string | LatLng, options?: SnapshotOptions): Promise<SnapshotDataset>;
952
+ /**
953
+ * A {@link PlacesProvider} backed by a stored {@link SnapshotDataset} — answers
954
+ * nearby queries entirely from memory, with no network. Use a "lat,lng" query
955
+ * (no geocoding) for a fully offline pipeline.
956
+ */
957
+ declare class DatasetPlacesProvider implements PlacesProvider {
958
+ readonly name = "dataset";
959
+ private readonly pois;
960
+ constructor(dataset: SnapshotDataset);
961
+ findNearby(center: LatLng, options: NearbyOptions): Promise<Poi[]>;
962
+ }
963
+
964
+ /**
965
+ * @proximap/core — geospatial engine for places, proximity, and amenities.
966
+ *
967
+ * Defaults to OpenStreetMap (Nominatim + Overpass) with no API keys, but every
968
+ * stage is pluggable via the provider interfaces in {@link ./types}.
969
+ */
970
+ /** Library version, kept in sync with package.json. */
971
+ declare const VERSION = "1.0.0";
972
+
973
+ export { CATEGORIES, CATEGORY_LABELS, type Categorization, type Category, type CategoryGap, type CategoryScore, type CategorySelector, type CategoryWeight, type CompareOptions, type ComparisonReport, DEFAULT_DAILY_NEEDS, DEFAULT_USER_AGENT, DEFAULT_WALK_CATEGORIES, DatasetPlacesProvider, type DimensionWinner, type DisambiguateOptions, type Disambiguation, type ErrandOptions, type ErrandPlan, type ErrandStop, type FacetFilters, type FacetPredicate, type FindNearbyOptions, type GapOptions, type GapReport, type GeoJsonFeature, type GeoJsonFeatureCollection, type GeocodeOptions, type GeocodingProvider, HaversineRoutingProvider, HttpError, InMemoryCache, type LatLng, type LocationScore, MODE_SPEED_MPS, type NearbyOptions, type NearbyResult, type NearestMatch, NominatimGeocoder, type NominatimOptions, ODBL_ATTRIBUTION, type OpenState, type OpeningEvaluation, type OsrmOptions, OsrmRoutingProvider, type OverpassOptions, OverpassPlacesProvider, type Place, type PlacesProvider, type Poi, type PolygonRing, type RankOptions, type RankedLocation, type RankedPoi, RateLimiter, type ReachableOptions, type ReachableResult, type RequestCache, type RequestOptions, type ResolveOriginOptions, type ResolvedCategories, type RouteMetric, type RoutingProvider, type RoutingRequestOptions, type ScoreInput, type SnapshotDataset, type SnapshotOptions, type TravelMode, VERSION, type ValhallaOptions, ValhallaRoutingProvider, type WalkabilityDecay, type WalkabilityOptions, type WalkabilityReport, accessibleScorer, buildOverpassQuery, buildTargetedOverpassQuery, categorize, categoryVocabulary, circlePolygon, compareLocations, compileFacets, completenessOf, dedupePois, detectGaps, disambiguateLocation, findNearbyAmenities, formatDistance, formatDuration, haversineMeters, isCategory, isKnownTerm, isOpenAt, lastVerifiedOf, matchesFacets, nearestMatchingPoi, parseCoordinates, planErrands, pointInPolygon, rankByProximity, reachableAmenities, requestJson, resolveCategories, resolveOrigin, selectorToOverpassFilter, snapshotArea, suggestCategories, tagsMatchAnySelector, tagsMatchSelector, toCSV, toGeoJSON, walkSubScore, walkabilityScore };