@unmap/geocoding 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/dist/index.d.ts CHANGED
@@ -46,7 +46,11 @@ interface NearbyOptions {
46
46
  }
47
47
  declare class GeocoderError extends Error {
48
48
  readonly status: number;
49
- constructor(status: number, message: string);
49
+ /** The gateway's machine-readable `code`, when the response carried one (e.g. `unauthorized`). */
50
+ readonly code?: string | undefined;
51
+ constructor(status: number, message: string,
52
+ /** The gateway's machine-readable `code`, when the response carried one (e.g. `unauthorized`). */
53
+ code?: string | undefined);
50
54
  }
51
55
  /** Typed client for the unmap geocoding API (gateway `/geocode/*`). */
52
56
  declare class Geocoder {
package/dist/index.js CHANGED
@@ -1,13 +1,27 @@
1
1
  // src/index.ts
2
2
  import { DEFAULT_GATEWAY } from "@unmap/core";
3
3
  var GeocoderError = class extends Error {
4
- constructor(status, message) {
4
+ constructor(status, message, code) {
5
5
  super(message);
6
6
  this.status = status;
7
+ this.code = code;
7
8
  this.name = "GeocoderError";
8
9
  }
9
10
  status;
11
+ code;
10
12
  };
13
+ async function errorFrom(res, path) {
14
+ const fallback = `geocode ${res.status} on ${path}`;
15
+ let body;
16
+ try {
17
+ body = await res.json();
18
+ } catch {
19
+ body = void 0;
20
+ }
21
+ const reason = typeof body?.error === "string" ? body.error : void 0;
22
+ const code = typeof body?.code === "string" ? body.code : void 0;
23
+ return new GeocoderError(res.status, reason ? `${reason} (${fallback})` : fallback, code);
24
+ }
11
25
  var Geocoder = class {
12
26
  key;
13
27
  base;
@@ -60,7 +74,7 @@ var Geocoder = class {
60
74
  signal,
61
75
  headers: { accept: "application/json" }
62
76
  });
63
- if (!res.ok) throw new GeocoderError(res.status, `geocode ${res.status} on ${path}`);
77
+ if (!res.ok) throw await errorFrom(res, path);
64
78
  const body = await res.json();
65
79
  return Array.isArray(body) ? body : body.results ?? [];
66
80
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { DEFAULT_GATEWAY } from \"@unmap/core\";\n\nexport interface GeocoderOptions {\n key: string;\n gateway?: string;\n /** Override the fetch implementation (tests, non-global environments). */\n fetch?: typeof fetch;\n}\n\nexport type GeocodeLayer = \"address\" | \"street\" | \"locality\" | \"region\" | \"poi\";\n\nexport interface GeocodeResult {\n id: string;\n name: string;\n layer: GeocodeLayer;\n lng: number;\n lat: number;\n score?: number;\n /** Canonical category id (e.g. \"health.pharmacy\") when the feature carries one. */\n category?: string;\n /** Spheroidal metres from the query anchor; only `/geocode/nearby` results carry it. */\n distance?: number;\n names?: Record<string, string>;\n address?: {\n housenumber?: string;\n street?: string;\n city?: string;\n region?: string;\n postcode?: string;\n country?: string;\n };\n}\n\nexport interface SearchOptions {\n limit?: number;\n lang?: string;\n /** [minLng, minLat, maxLng, maxLat] bias/filter. */\n bbox?: [number, number, number, number];\n signal?: AbortSignal;\n}\n\nexport interface NearbyOptions {\n /** [lng, lat] anchor to sort by distance from. Exactly one of `near` or `bbox`. */\n near?: [number, number];\n /** [minLng, minLat, maxLng, maxLat]; the API anchors at its centre. */\n bbox?: [number, number, number, number];\n /** Search radius in metres (API default 5000, clamped to [50, 50000]). */\n radius?: number;\n limit?: number;\n lang?: string;\n signal?: AbortSignal;\n}\n\nexport class GeocoderError extends Error {\n constructor(\n public readonly status: number,\n message: string,\n ) {\n super(message);\n this.name = \"GeocoderError\";\n }\n}\n\n/** Typed client for the unmap geocoding API (gateway `/geocode/*`). */\nexport class Geocoder {\n private readonly key: string;\n private readonly base: string;\n private readonly fetchImpl: typeof fetch;\n\n constructor(opts: GeocoderOptions) {\n this.key = opts.key;\n this.base = (opts.gateway ?? DEFAULT_GATEWAY).replace(/\\/+$/, \"\");\n // Bound to globalThis: stored on an instance and called as `this.fetchImpl(...)`, a bare\n // `fetch` reference is invoked with the Geocoder as its receiver, and browsers reject that\n // with \"Illegal invocation\". Node is lenient, so this only ever surfaces in the browser.\n this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);\n }\n\n /** Full-text search. */\n search(q: string, opts: SearchOptions = {}): Promise<GeocodeResult[]> {\n return this.query(\"/geocode/search\", { q, ...toParams(opts) }, opts.signal);\n }\n\n /** Type-ahead suggestions for partial input. */\n autocomplete(q: string, opts: SearchOptions = {}): Promise<GeocodeResult[]> {\n return this.query(\"/geocode/autocomplete\", { q, ...toParams(opts) }, opts.signal);\n }\n\n /**\n * Category search: POIs of a kind near a point, sorted by distance (metres, in each\n * result's `distance`). `category` takes canonical taxonomy ids (\"health.pharmacy\") or\n * plain words in English or French (\"pharmacy\", \"pharmacie\") — the gateway resolves\n * aliases, so this client passes the value through untouched.\n */\n nearby(category: string | string[], opts: NearbyOptions = {}): Promise<GeocodeResult[]> {\n return this.query(\n \"/geocode/nearby\",\n {\n category: Array.isArray(category) ? category.join(\",\") : category,\n near: opts.near ? opts.near.join(\",\") : undefined,\n bbox: opts.bbox ? opts.bbox.join(\",\") : undefined,\n radius: opts.radius != null ? String(opts.radius) : undefined,\n limit: opts.limit != null ? String(opts.limit) : undefined,\n lang: opts.lang,\n },\n opts.signal,\n );\n }\n\n /** Reverse geocode a coordinate. */\n reverse(\n lng: number,\n lat: number,\n opts: { lang?: string; signal?: AbortSignal } = {},\n ): Promise<GeocodeResult[]> {\n return this.query(\n \"/geocode/reverse\",\n { lon: String(lng), lat: String(lat), lang: opts.lang },\n opts.signal,\n );\n }\n\n private async query(\n path: string,\n params: Record<string, string | undefined>,\n signal?: AbortSignal,\n ): Promise<GeocodeResult[]> {\n const q = new URLSearchParams({ key: this.key });\n for (const [k, v] of Object.entries(params)) if (v != null && v !== \"\") q.set(k, v);\n const res = await this.fetchImpl(`${this.base}${path}?${q}`, {\n signal,\n headers: { accept: \"application/json\" },\n });\n if (!res.ok) throw new GeocoderError(res.status, `geocode ${res.status} on ${path}`);\n const body = (await res.json()) as { results?: GeocodeResult[] } | GeocodeResult[];\n return Array.isArray(body) ? body : (body.results ?? []);\n }\n}\n\nfunction toParams(o: SearchOptions): Record<string, string | undefined> {\n return {\n limit: o.limit != null ? String(o.limit) : undefined,\n lang: o.lang,\n bbox: o.bbox ? o.bbox.join(\",\") : undefined,\n };\n}\n"],"mappings":";AAAA,SAAS,uBAAuB;AAqDzB,IAAM,gBAAN,cAA4B,MAAM;AAAA,EACvC,YACkB,QAChB,SACA;AACA,UAAM,OAAO;AAHG;AAIhB,SAAK,OAAO;AAAA,EACd;AAAA,EALkB;AAMpB;AAGO,IAAM,WAAN,MAAe;AAAA,EACH;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,MAAuB;AACjC,SAAK,MAAM,KAAK;AAChB,SAAK,QAAQ,KAAK,WAAW,iBAAiB,QAAQ,QAAQ,EAAE;AAIhE,SAAK,YAAY,KAAK,SAAS,WAAW,MAAM,KAAK,UAAU;AAAA,EACjE;AAAA;AAAA,EAGA,OAAO,GAAW,OAAsB,CAAC,GAA6B;AACpE,WAAO,KAAK,MAAM,mBAAmB,EAAE,GAAG,GAAG,SAAS,IAAI,EAAE,GAAG,KAAK,MAAM;AAAA,EAC5E;AAAA;AAAA,EAGA,aAAa,GAAW,OAAsB,CAAC,GAA6B;AAC1E,WAAO,KAAK,MAAM,yBAAyB,EAAE,GAAG,GAAG,SAAS,IAAI,EAAE,GAAG,KAAK,MAAM;AAAA,EAClF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,OAAO,UAA6B,OAAsB,CAAC,GAA6B;AACtF,WAAO,KAAK;AAAA,MACV;AAAA,MACA;AAAA,QACE,UAAU,MAAM,QAAQ,QAAQ,IAAI,SAAS,KAAK,GAAG,IAAI;AAAA,QACzD,MAAM,KAAK,OAAO,KAAK,KAAK,KAAK,GAAG,IAAI;AAAA,QACxC,MAAM,KAAK,OAAO,KAAK,KAAK,KAAK,GAAG,IAAI;AAAA,QACxC,QAAQ,KAAK,UAAU,OAAO,OAAO,KAAK,MAAM,IAAI;AAAA,QACpD,OAAO,KAAK,SAAS,OAAO,OAAO,KAAK,KAAK,IAAI;AAAA,QACjD,MAAM,KAAK;AAAA,MACb;AAAA,MACA,KAAK;AAAA,IACP;AAAA,EACF;AAAA;AAAA,EAGA,QACE,KACA,KACA,OAAgD,CAAC,GACvB;AAC1B,WAAO,KAAK;AAAA,MACV;AAAA,MACA,EAAE,KAAK,OAAO,GAAG,GAAG,KAAK,OAAO,GAAG,GAAG,MAAM,KAAK,KAAK;AAAA,MACtD,KAAK;AAAA,IACP;AAAA,EACF;AAAA,EAEA,MAAc,MACZ,MACA,QACA,QAC0B;AAC1B,UAAM,IAAI,IAAI,gBAAgB,EAAE,KAAK,KAAK,IAAI,CAAC;AAC/C,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,MAAM,EAAG,KAAI,KAAK,QAAQ,MAAM,GAAI,GAAE,IAAI,GAAG,CAAC;AAClF,UAAM,MAAM,MAAM,KAAK,UAAU,GAAG,KAAK,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI;AAAA,MAC3D;AAAA,MACA,SAAS,EAAE,QAAQ,mBAAmB;AAAA,IACxC,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,OAAM,IAAI,cAAc,IAAI,QAAQ,WAAW,IAAI,MAAM,OAAO,IAAI,EAAE;AACnF,UAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,WAAO,MAAM,QAAQ,IAAI,IAAI,OAAQ,KAAK,WAAW,CAAC;AAAA,EACxD;AACF;AAEA,SAAS,SAAS,GAAsD;AACtE,SAAO;AAAA,IACL,OAAO,EAAE,SAAS,OAAO,OAAO,EAAE,KAAK,IAAI;AAAA,IAC3C,MAAM,EAAE;AAAA,IACR,MAAM,EAAE,OAAO,EAAE,KAAK,KAAK,GAAG,IAAI;AAAA,EACpC;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { DEFAULT_GATEWAY } from \"@unmap/core\";\n\nexport interface GeocoderOptions {\n key: string;\n gateway?: string;\n /** Override the fetch implementation (tests, non-global environments). */\n fetch?: typeof fetch;\n}\n\nexport type GeocodeLayer = \"address\" | \"street\" | \"locality\" | \"region\" | \"poi\";\n\nexport interface GeocodeResult {\n id: string;\n name: string;\n layer: GeocodeLayer;\n lng: number;\n lat: number;\n score?: number;\n /** Canonical category id (e.g. \"health.pharmacy\") when the feature carries one. */\n category?: string;\n /** Spheroidal metres from the query anchor; only `/geocode/nearby` results carry it. */\n distance?: number;\n names?: Record<string, string>;\n address?: {\n housenumber?: string;\n street?: string;\n city?: string;\n region?: string;\n postcode?: string;\n country?: string;\n };\n}\n\nexport interface SearchOptions {\n limit?: number;\n lang?: string;\n /** [minLng, minLat, maxLng, maxLat] bias/filter. */\n bbox?: [number, number, number, number];\n signal?: AbortSignal;\n}\n\nexport interface NearbyOptions {\n /** [lng, lat] anchor to sort by distance from. Exactly one of `near` or `bbox`. */\n near?: [number, number];\n /** [minLng, minLat, maxLng, maxLat]; the API anchors at its centre. */\n bbox?: [number, number, number, number];\n /** Search radius in metres (API default 5000, clamped to [50, 50000]). */\n radius?: number;\n limit?: number;\n lang?: string;\n signal?: AbortSignal;\n}\n\nexport class GeocoderError extends Error {\n constructor(\n public readonly status: number,\n message: string,\n /** The gateway's machine-readable `code`, when the response carried one (e.g. `unauthorized`). */\n public readonly code?: string,\n ) {\n super(message);\n this.name = \"GeocoderError\";\n }\n}\n\n/**\n * The gateway answers every error with a JSON body whose `error` says what went wrong\n * (\"mode must be one of auto, car, bicycle, pedestrian, truck\", \"q required\"). Put that in the\n * message so a caller who logs the thrown error reads the reason, not just the status.\n */\nasync function errorFrom(res: { status: number; json(): Promise<unknown> }, path: string): Promise<GeocoderError> {\n const fallback = `geocode ${res.status} on ${path}`;\n let body: { error?: unknown; code?: unknown } | undefined;\n try {\n body = (await res.json()) as { error?: unknown; code?: unknown };\n } catch {\n body = undefined;\n }\n const reason = typeof body?.error === \"string\" ? body.error : undefined;\n const code = typeof body?.code === \"string\" ? body.code : undefined;\n return new GeocoderError(res.status, reason ? `${reason} (${fallback})` : fallback, code);\n}\n\n/** Typed client for the unmap geocoding API (gateway `/geocode/*`). */\nexport class Geocoder {\n private readonly key: string;\n private readonly base: string;\n private readonly fetchImpl: typeof fetch;\n\n constructor(opts: GeocoderOptions) {\n this.key = opts.key;\n this.base = (opts.gateway ?? DEFAULT_GATEWAY).replace(/\\/+$/, \"\");\n // Bound to globalThis: stored on an instance and called as `this.fetchImpl(...)`, a bare\n // `fetch` reference is invoked with the Geocoder as its receiver, and browsers reject that\n // with \"Illegal invocation\". Node is lenient, so this only ever surfaces in the browser.\n this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);\n }\n\n /** Full-text search. */\n search(q: string, opts: SearchOptions = {}): Promise<GeocodeResult[]> {\n return this.query(\"/geocode/search\", { q, ...toParams(opts) }, opts.signal);\n }\n\n /** Type-ahead suggestions for partial input. */\n autocomplete(q: string, opts: SearchOptions = {}): Promise<GeocodeResult[]> {\n return this.query(\"/geocode/autocomplete\", { q, ...toParams(opts) }, opts.signal);\n }\n\n /**\n * Category search: POIs of a kind near a point, sorted by distance (metres, in each\n * result's `distance`). `category` takes canonical taxonomy ids (\"health.pharmacy\") or\n * plain words in English or French (\"pharmacy\", \"pharmacie\") — the gateway resolves\n * aliases, so this client passes the value through untouched.\n */\n nearby(category: string | string[], opts: NearbyOptions = {}): Promise<GeocodeResult[]> {\n return this.query(\n \"/geocode/nearby\",\n {\n category: Array.isArray(category) ? category.join(\",\") : category,\n near: opts.near ? opts.near.join(\",\") : undefined,\n bbox: opts.bbox ? opts.bbox.join(\",\") : undefined,\n radius: opts.radius != null ? String(opts.radius) : undefined,\n limit: opts.limit != null ? String(opts.limit) : undefined,\n lang: opts.lang,\n },\n opts.signal,\n );\n }\n\n /** Reverse geocode a coordinate. */\n reverse(\n lng: number,\n lat: number,\n opts: { lang?: string; signal?: AbortSignal } = {},\n ): Promise<GeocodeResult[]> {\n return this.query(\n \"/geocode/reverse\",\n { lon: String(lng), lat: String(lat), lang: opts.lang },\n opts.signal,\n );\n }\n\n private async query(\n path: string,\n params: Record<string, string | undefined>,\n signal?: AbortSignal,\n ): Promise<GeocodeResult[]> {\n const q = new URLSearchParams({ key: this.key });\n for (const [k, v] of Object.entries(params)) if (v != null && v !== \"\") q.set(k, v);\n const res = await this.fetchImpl(`${this.base}${path}?${q}`, {\n signal,\n headers: { accept: \"application/json\" },\n });\n if (!res.ok) throw await errorFrom(res, path);\n const body = (await res.json()) as { results?: GeocodeResult[] } | GeocodeResult[];\n return Array.isArray(body) ? body : (body.results ?? []);\n }\n}\n\nfunction toParams(o: SearchOptions): Record<string, string | undefined> {\n return {\n limit: o.limit != null ? String(o.limit) : undefined,\n lang: o.lang,\n bbox: o.bbox ? o.bbox.join(\",\") : undefined,\n };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unmap/geocoding",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Typed client for the unmap geocoding API",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -38,7 +38,7 @@
38
38
  "access": "public"
39
39
  },
40
40
  "dependencies": {
41
- "@unmap/core": "0.3.0"
41
+ "@unmap/core": "0.5.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "tsup": "^8.3.0",