@unmap/geocoding 0.12.0 → 0.14.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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- import { DEFAULT_GATEWAY } from "@unmap/core";
2
+ import { DEFAULT_GATEWAY as DEFAULT_GATEWAY2, UnmapError as UnmapError2, fetchWithRetry as fetchWithRetry2, parseRetryAfter as parseRetryAfter2 } from "@unmap/core";
3
3
 
4
4
  // src/legal-land.ts
5
5
  var SEP = String.raw`(?:\s*-\s*|\s+)`;
@@ -651,6 +651,317 @@ function parseFocus(raw) {
651
651
  return { focus: [lng, lat] };
652
652
  }
653
653
 
654
+ // src/treaties.ts
655
+ var INCLUDE_VALUES = ["treaties"];
656
+ var COMMUNITY_INCLUDE_VALUES = ["territories", "acknowledgements"];
657
+ function parseInclude(raw, options = {}) {
658
+ const out = { treaties: false, territories: false, acknowledgements: false };
659
+ const known = options.community ? [...INCLUDE_VALUES, ...COMMUNITY_INCLUDE_VALUES] : [...INCLUDE_VALUES];
660
+ for (const token of (raw ?? "").split(",")) {
661
+ const value = token.trim();
662
+ if (value === "") continue;
663
+ if (value === "treaties") {
664
+ out.treaties = true;
665
+ continue;
666
+ }
667
+ if (COMMUNITY_INCLUDE_VALUES.includes(value)) {
668
+ if (!options.community) {
669
+ return { error: `include "${value}" is not available: no community source has been admitted` };
670
+ }
671
+ out[value] = true;
672
+ continue;
673
+ }
674
+ return { error: `unknown include "${value}". known: ${known.join(", ")}` };
675
+ }
676
+ return out;
677
+ }
678
+ var VERSION_SHAPE = /^(\d{4})-(\d{2})-(\d{2})\.[0-9a-f]{12}$/;
679
+ function isTreatiesVersion(value) {
680
+ const m = VERSION_SHAPE.exec(value);
681
+ if (!m) return false;
682
+ const [year, month, day] = [Number(m[1]), Number(m[2]), Number(m[3])];
683
+ const date = new Date(Date.UTC(year, month - 1, day));
684
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
685
+ }
686
+ function parseTreatiesVersion(raw, treatiesIncluded) {
687
+ if (raw === void 0) return { version: void 0 };
688
+ if (!treatiesIncluded) return { error: "treaties_version requires include=treaties" };
689
+ if (!isTreatiesVersion(raw)) {
690
+ return { error: "treaties_version must be YYYY-MM-DD. followed by 12 lowercase hex characters" };
691
+ }
692
+ return { version: raw };
693
+ }
694
+ function pickTreatyLabel(labels, lang) {
695
+ const preferred = lang.toLowerCase().split("-")[0] === "fr" ? "fr" : "en";
696
+ const other = preferred === "fr" ? "en" : "fr";
697
+ for (const l of [preferred, other]) {
698
+ const text = labels[l];
699
+ if (text !== void 0 && text.trim() !== "") return { text, lang: l };
700
+ }
701
+ return void 0;
702
+ }
703
+ function orderTreatyMatches(matches) {
704
+ const byId = /* @__PURE__ */ new Map();
705
+ for (const m of matches) if (!byId.has(m.id)) byId.set(m.id, m);
706
+ const rank = (c) => c === "historic" ? 0 : 1;
707
+ return [...byId.values()].sort(
708
+ (a, b) => rank(a.category) - rank(b.category) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
709
+ );
710
+ }
711
+ var CATEGORIES = /* @__PURE__ */ new Set(["historic", "modern"]);
712
+ var BASES = /* @__PURE__ */ new Set(["result_point", "input_point"]);
713
+ var KINDS = /* @__PURE__ */ new Set([
714
+ "treaty_area",
715
+ "overall_treaty_area",
716
+ "settlement_land",
717
+ "rights_area",
718
+ "other",
719
+ "unspecified"
720
+ ]);
721
+ var REPRESENTATIONS = /* @__PURE__ */ new Set(["approximate", "illustrative", "unspecified"]);
722
+ function isRecord(value) {
723
+ return typeof value === "object" && value !== null && !Array.isArray(value);
724
+ }
725
+ function fail(message) {
726
+ throw new TypeError(`invalid treaty context: ${message}`);
727
+ }
728
+ function isHttpUrl(value) {
729
+ if (typeof value !== "string") return false;
730
+ try {
731
+ const url = new URL(value);
732
+ return url.protocol === "https:" || url.protocol === "http:";
733
+ } catch {
734
+ return false;
735
+ }
736
+ }
737
+ function checkLabels(value, field) {
738
+ if (!isRecord(value)) fail(`${field} must be an object`);
739
+ for (const [k, v] of Object.entries(value)) {
740
+ if (k !== "en" && k !== "fr" || typeof v !== "string") fail(`${field} may only carry en/fr strings`);
741
+ }
742
+ }
743
+ function checkMatch(value, i) {
744
+ const at = `matches[${i}]`;
745
+ if (!isRecord(value)) fail(`${at} must be an object`);
746
+ for (const key of ["id", "source_id", "source_record_id", "name"]) {
747
+ if (typeof value[key] !== "string" || value[key] === "") fail(`${at}.${key} must be a non-empty string`);
748
+ }
749
+ if (!CATEGORIES.has(value.category)) fail(`${at}.category`);
750
+ if (value.name_lang !== "en" && value.name_lang !== "fr") fail(`${at}.name_lang`);
751
+ checkLabels(value.names, `${at}.names`);
752
+ if (value.subtype !== void 0 && typeof value.subtype !== "string") fail(`${at}.subtype`);
753
+ if (value.subtypes !== void 0) checkLabels(value.subtypes, `${at}.subtypes`);
754
+ if (!KINDS.has(value.geography_kind)) fail(`${at}.geography_kind`);
755
+ if (!REPRESENTATIONS.has(value.representation)) fail(`${at}.representation`);
756
+ if (!isHttpUrl(value.source_url)) fail(`${at}.source_url must be an http(s) URL`);
757
+ }
758
+ function parseTreatyContext(value) {
759
+ if (!isRecord(value)) fail("must be an object");
760
+ if (value.schema_version !== 1) fail("schema_version must be 1");
761
+ if (value.status !== "ok" && value.status !== "unavailable") fail("status");
762
+ const version = value.dataset_version;
763
+ if (version !== null && (typeof version !== "string" || !isTreatiesVersion(version))) {
764
+ fail("dataset_version must be a version or null");
765
+ }
766
+ if (value.status === "ok" && version === null) fail("an ok context needs a dataset_version");
767
+ const point2 = value.point;
768
+ if (!isRecord(point2)) fail("point must be an object");
769
+ if (typeof point2.lng !== "number" || !Number.isFinite(point2.lng) || point2.lng < -180 || point2.lng > 180) {
770
+ fail("point.lng");
771
+ }
772
+ if (typeof point2.lat !== "number" || !Number.isFinite(point2.lat) || point2.lat < -90 || point2.lat > 90) {
773
+ fail("point.lat");
774
+ }
775
+ if (!BASES.has(point2.basis)) fail("point.basis");
776
+ const coverage = value.coverage;
777
+ if (!isRecord(coverage) || coverage.scope !== "Canada" || coverage.completeness !== "partial") fail("coverage");
778
+ if (value.notice_code !== "source_limitations_apply") fail("notice_code");
779
+ if (!Array.isArray(value.matches)) fail("matches must be an array");
780
+ if (value.status === "unavailable" && value.matches.length > 0) fail("an unavailable context carries no matches");
781
+ value.matches.forEach(checkMatch);
782
+ return value;
783
+ }
784
+ function unavailableTreatyContext(point2) {
785
+ return {
786
+ schema_version: 1,
787
+ dataset_version: null,
788
+ status: "unavailable",
789
+ point: { lng: point2.lng, lat: point2.lat, basis: point2.basis },
790
+ coverage: { scope: "Canada", completeness: "partial" },
791
+ matches: [],
792
+ notice_code: "source_limitations_apply"
793
+ };
794
+ }
795
+
796
+ // src/community.ts
797
+ function pickCommunityName(names, lang, defaultLang) {
798
+ const want = lang.toLowerCase().split("-")[0];
799
+ for (const [tag, text] of Object.entries(names)) {
800
+ if (tag.toLowerCase().split("-")[0] === want && text.trim() !== "") return { text, lang: tag };
801
+ }
802
+ const fallback = names[defaultLang];
803
+ return fallback !== void 0 && fallback.trim() !== "" ? { text: fallback, lang: defaultLang } : void 0;
804
+ }
805
+ function point(p) {
806
+ return { lng: p.lng, lat: p.lat, basis: p.basis };
807
+ }
808
+ function emptyTerritoryContext(p, status) {
809
+ return {
810
+ schema_version: 1,
811
+ dataset_version: null,
812
+ status,
813
+ point: point(p),
814
+ coverage: { scope: "source_defined", completeness: "partial" },
815
+ matches: [],
816
+ notice_code: "source_limitations_apply"
817
+ };
818
+ }
819
+ function emptyAcknowledgementContext(p, status) {
820
+ return {
821
+ schema_version: 1,
822
+ dataset_version: null,
823
+ status,
824
+ point: point(p),
825
+ resources: [],
826
+ notice_code: "source_limitations_apply"
827
+ };
828
+ }
829
+ function isRecord2(v) {
830
+ return typeof v === "object" && v !== null && !Array.isArray(v);
831
+ }
832
+ function fail2(message) {
833
+ throw new TypeError(`invalid community context: ${message}`);
834
+ }
835
+ function isHttpUrl2(v) {
836
+ if (typeof v !== "string") return false;
837
+ try {
838
+ const { protocol } = new URL(v);
839
+ return protocol === "https:" || protocol === "http:";
840
+ } catch {
841
+ return false;
842
+ }
843
+ }
844
+ function checkEnvelope(v, list) {
845
+ if (v.schema_version !== 1) fail2("schema_version must be 1");
846
+ if (v.status !== "ok" && v.status !== "not_enabled" && v.status !== "unavailable") fail2("status");
847
+ const version = v.dataset_version;
848
+ if (version !== null && (typeof version !== "string" || !isTreatiesVersion(version))) fail2("dataset_version");
849
+ if (v.status === "ok" && version === null) fail2("an ok context needs a dataset_version");
850
+ if (v.notice_code !== "source_limitations_apply") fail2("notice_code");
851
+ const p = v.point;
852
+ if (!isRecord2(p) || typeof p.lng !== "number" || typeof p.lat !== "number" || !Number.isFinite(p.lng) || !Number.isFinite(p.lat)) {
853
+ fail2("point");
854
+ }
855
+ const items = v[list];
856
+ if (!Array.isArray(items)) fail2(`${list} must be an array`);
857
+ if (v.status !== "ok" && items.length > 0) fail2(`a ${String(v.status)} context carries no ${list}`);
858
+ return items;
859
+ }
860
+ function parseTerritoryContext(value) {
861
+ if (!isRecord2(value)) fail2("must be an object");
862
+ const items = checkEnvelope(value, "matches");
863
+ items.forEach((m, i) => {
864
+ if (!isRecord2(m)) fail2(`matches[${i}]`);
865
+ for (const k of ["id", "source_id", "source_record_id", "name", "name_lang", "attribution"]) {
866
+ if (typeof m[k] !== "string" || m[k] === "") fail2(`matches[${i}].${k}`);
867
+ }
868
+ if (!Array.isArray(m.community_ids) || m.community_ids.some((c) => typeof c !== "string")) fail2(`matches[${i}].community_ids`);
869
+ if (!isHttpUrl2(m.source_url)) fail2(`matches[${i}].source_url must be an http(s) URL`);
870
+ });
871
+ return value;
872
+ }
873
+ function parseAcknowledgementContext(value) {
874
+ if (!isRecord2(value)) fail2("must be an object");
875
+ const items = checkEnvelope(value, "resources");
876
+ items.forEach((r, i) => {
877
+ if (!isRecord2(r)) fail2(`resources[${i}]`);
878
+ for (const k of ["id", "source_id", "title", "language", "attribution", "applicability", "reviewed_at", "content_version"]) {
879
+ if (typeof r[k] !== "string" || r[k] === "") fail2(`resources[${i}].${k}`);
880
+ }
881
+ if (!isHttpUrl2(r.url)) fail2(`resources[${i}].url must be an http(s) URL`);
882
+ if (!Array.isArray(r.applies_to) || r.applies_to.length === 0 || r.applies_to.some((t) => typeof t !== "string")) {
883
+ fail2(`resources[${i}].applies_to must name at least one territory`);
884
+ }
885
+ if (r.text !== void 0 && typeof r.text !== "string") fail2(`resources[${i}].text`);
886
+ });
887
+ return value;
888
+ }
889
+
890
+ // src/treaty-overlay.ts
891
+ function treatyLayerIds(category) {
892
+ return [`overlay_treaties_fill_${category}`, `overlay_treaties_line_${category}`];
893
+ }
894
+ var CATEGORIES2 = ["historic", "modern"];
895
+ function createTreatyOverlay(map, client, options = {}) {
896
+ const visibility = { historic: options.historic ?? true, modern: options.modern ?? true };
897
+ let lang = options.lang ?? "en";
898
+ let latest = 0;
899
+ let destroyed = false;
900
+ let drawnVersion;
901
+ const allLayers = () => CATEGORIES2.flatMap((c) => treatyLayerIds(c)).filter((id) => map.getLayer(id));
902
+ const apply = () => {
903
+ for (const category of CATEGORIES2) {
904
+ for (const id of treatyLayerIds(category)) {
905
+ if (map.getLayer(id)) map.setLayoutProperty(id, "visibility", visibility[category] ? "visible" : "none");
906
+ }
907
+ }
908
+ };
909
+ const hidden = () => CATEGORIES2.filter((c) => !visibility[c]);
910
+ async function select(point2, version, drawn) {
911
+ const id = ++latest;
912
+ try {
913
+ const results = await client.reverse(point2[0], point2[1], {
914
+ include: ["treaties"],
915
+ lang,
916
+ limit: 1,
917
+ ...version ? { treatiesVersion: version } : {}
918
+ });
919
+ const context = results[0]?.treaties;
920
+ if (!context) throw new Error("the geocoder returned no treaty context");
921
+ const selection = { context, drawn, hidden: hidden() };
922
+ if (id === latest && !destroyed) options.onSelect?.(selection);
923
+ return selection;
924
+ } catch (err) {
925
+ if (id === latest && !destroyed) {
926
+ options.onError?.(err instanceof Error ? err : new Error(String(err)));
927
+ }
928
+ return void 0;
929
+ }
930
+ }
931
+ const onClick = (e) => {
932
+ const layers = allLayers();
933
+ const features = layers.length > 0 ? map.queryRenderedFeatures(e.point, { layers }) : [];
934
+ const drawn = [...new Set(features.map((f) => f.properties?.id).filter((v) => typeof v === "string"))].sort();
935
+ const version = features.map((f) => f.properties?.dataset_version).find((v) => typeof v === "string");
936
+ if (version) drawnVersion = version;
937
+ void select([e.lngLat.lng, e.lngLat.lat], version ?? drawnVersion, drawn);
938
+ };
939
+ map.on("styledata", apply);
940
+ if (options.interactive !== false) map.on("click", onClick);
941
+ apply();
942
+ return {
943
+ get visibility() {
944
+ return { ...visibility };
945
+ },
946
+ setVisibility(value) {
947
+ if (value.historic !== void 0) visibility.historic = value.historic;
948
+ if (value.modern !== void 0) visibility.modern = value.modern;
949
+ apply();
950
+ },
951
+ setLanguage(next) {
952
+ lang = next;
953
+ },
954
+ lookupAt(point2, datasetVersion) {
955
+ return select(point2, datasetVersion ?? drawnVersion, []);
956
+ },
957
+ destroy() {
958
+ destroyed = true;
959
+ map.off("styledata", apply);
960
+ map.off("click", onClick);
961
+ }
962
+ };
963
+ }
964
+
654
965
  // src/regions.ts
655
966
  var BASE = {
656
967
  ab: "AB",
@@ -822,7 +1133,7 @@ function parseQueryLimit(raw) {
822
1133
  }
823
1134
 
824
1135
  // src/rural-route.ts
825
- var KINDS = [
1136
+ var KINDS2 = [
826
1137
  { pattern: /\b(?:R\.?\s?R\.?|RURAL\s+ROUTE)\s*#?\s*(\d{1,3})\b/i, kind: "rural-route", numbered: true },
827
1138
  { pattern: /\b(?:S\.?\s?S\.?|SUBURBAN\s+SERVICE)\s*#?\s*(\d{1,3})\b/i, kind: "suburban-service", numbered: true },
828
1139
  { pattern: /\b(?:M\.?\s?R\.?|MOBILE\s+ROUTE)\s*#?\s*(\d{1,3})\b/i, kind: "mobile-route", numbered: true },
@@ -842,7 +1153,7 @@ function parseRuralRoute(q) {
842
1153
  if (typeof q !== "string") return null;
843
1154
  const raw = q.trim();
844
1155
  if (raw === "") return null;
845
- const matched = KINDS.map((k) => ({ ...k, match: k.pattern.exec(raw) })).find((k) => k.match !== null);
1156
+ const matched = KINDS2.map((k) => ({ ...k, match: k.pattern.exec(raw) })).find((k) => k.match !== null);
846
1157
  if (!matched?.match) return null;
847
1158
  let rest = raw.replace(matched.pattern, " ");
848
1159
  const parsed = { raw, kind: matched.kind, remainder: "" };
@@ -860,18 +1171,105 @@ function looksLikeRuralRoute(q) {
860
1171
  return parseRuralRoute(q) != null;
861
1172
  }
862
1173
 
1174
+ // src/places.ts
1175
+ import { DEFAULT_GATEWAY, UnmapError, fetchWithRetry, parseRetryAfter } from "@unmap/core";
1176
+ var PlacesError = class extends UnmapError {
1177
+ constructor(status, message, code, retryAfterSeconds) {
1178
+ super(status, message, code, retryAfterSeconds);
1179
+ this.name = "PlacesError";
1180
+ }
1181
+ };
1182
+ async function errorFrom(res, path) {
1183
+ const fallback = `places ${res.status} on ${path}`;
1184
+ let body;
1185
+ try {
1186
+ body = await res.json();
1187
+ } catch {
1188
+ body = void 0;
1189
+ }
1190
+ const reason = typeof body?.error === "string" ? body.error : void 0;
1191
+ const code = typeof body?.code === "string" ? body.code : void 0;
1192
+ const retryAfter = parseRetryAfter(res.headers?.get("retry-after") ?? null) ?? void 0;
1193
+ return new PlacesError(res.status, reason ? `${reason} (${fallback})` : fallback, code, retryAfter);
1194
+ }
1195
+ var Places = class {
1196
+ key;
1197
+ base;
1198
+ fetchImpl;
1199
+ retry;
1200
+ constructor(opts) {
1201
+ this.retry = opts.retry;
1202
+ this.key = opts.key;
1203
+ this.base = (opts.gateway ?? DEFAULT_GATEWAY).replace(/\/+$/, "");
1204
+ this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
1205
+ }
1206
+ /**
1207
+ * Search places by name.
1208
+ *
1209
+ * Unlike `Geocoder.search()`, which returns whatever best matches and for "Calgary" is the city
1210
+ * itself, this returns places. A missing query is 400 `bad_request` (`q is required`). A bbox
1211
+ * corner outside WGS84 is the same status and code. A box outside Canada stays a filter.
1212
+ */
1213
+ search(q, opts = {}) {
1214
+ return this.request("/places/search", {
1215
+ q,
1216
+ limit: opts.limit != null ? String(opts.limit) : void 0,
1217
+ lang: opts.lang,
1218
+ bbox: opts.bbox?.join(",")
1219
+ }, opts.signal);
1220
+ }
1221
+ /**
1222
+ * Places of a category near a point.
1223
+ *
1224
+ * `category` takes a taxonomy id (`health.pharmacy`) or a word in English or French
1225
+ * (`pharmacy`, `pharmacie`); the gateway resolves aliases, so `"boulangerie"` works. An anchor
1226
+ * outside Canada rejects with status 422 and code `outside_coverage`. A longitude or latitude
1227
+ * outside WGS84, including a bbox corner, rejects with status 400 and code `bad_request`.
1228
+ */
1229
+ nearby(category, opts = {}) {
1230
+ return this.request("/places/nearby", {
1231
+ category: Array.isArray(category) ? category.join(",") : String(category),
1232
+ near: opts.near?.join(","),
1233
+ bbox: opts.bbox?.join(","),
1234
+ radius: opts.radius != null ? String(opts.radius) : void 0,
1235
+ limit: opts.limit != null ? String(opts.limit) : void 0,
1236
+ lang: opts.lang
1237
+ }, opts.signal);
1238
+ }
1239
+ /**
1240
+ * One place by its unmap id.
1241
+ *
1242
+ * Throws `PlacesError` with status 404 when it does not exist. A malformed id is also a 404,
1243
+ * because from a caller's side "does not exist" and "is not a well-formed id" are the same
1244
+ * answer; the gateway's message distinguishes them as far as it honestly can.
1245
+ */
1246
+ get(id, opts = {}) {
1247
+ return this.request(`/places/${encodeURIComponent(id)}`, { lang: opts.lang }, opts.signal);
1248
+ }
1249
+ async request(path, params, signal) {
1250
+ const q = new URLSearchParams({ key: this.key });
1251
+ for (const [k, v] of Object.entries(params)) if (v != null && v !== "") q.set(k, v);
1252
+ const res = await fetchWithRetry(
1253
+ this.fetchImpl,
1254
+ `${this.base}${path}?${q}`,
1255
+ { signal, headers: { accept: "application/json" } },
1256
+ this.retry
1257
+ );
1258
+ if (!res.ok) throw await errorFrom(res, path);
1259
+ return await res.json();
1260
+ }
1261
+ };
1262
+
863
1263
  // src/index.ts
864
- var GeocoderError = class extends Error {
865
- constructor(status, message, code) {
866
- super(message);
867
- this.status = status;
868
- this.code = code;
1264
+ import { UnmapError as UnmapError3, parseRetryAfter as parseRetryAfter3 } from "@unmap/core";
1265
+ var GEOCODE_BATCH_MAX = 100;
1266
+ var GeocoderError = class extends UnmapError2 {
1267
+ constructor(status, message, code, retryAfterSeconds) {
1268
+ super(status, message, code, retryAfterSeconds);
869
1269
  this.name = "GeocoderError";
870
1270
  }
871
- status;
872
- code;
873
1271
  };
874
- async function errorFrom(res, path) {
1272
+ async function errorFrom2(res, path) {
875
1273
  const fallback = `geocode ${res.status} on ${path}`;
876
1274
  let body;
877
1275
  try {
@@ -881,21 +1279,46 @@ async function errorFrom(res, path) {
881
1279
  }
882
1280
  const reason = typeof body?.error === "string" ? body.error : void 0;
883
1281
  const code = typeof body?.code === "string" ? body.code : void 0;
884
- return new GeocoderError(res.status, reason ? `${reason} (${fallback})` : fallback, code);
1282
+ const retryAfter = parseRetryAfter2(res.headers?.get("retry-after") ?? null) ?? void 0;
1283
+ return new GeocoderError(res.status, reason ? `${reason} (${fallback})` : fallback, code, retryAfter);
885
1284
  }
886
1285
  var Geocoder = class {
887
1286
  key;
888
1287
  base;
889
1288
  fetchImpl;
1289
+ retry;
890
1290
  constructor(opts) {
891
1291
  this.key = opts.key;
892
- this.base = (opts.gateway ?? DEFAULT_GATEWAY).replace(/\/+$/, "");
1292
+ this.retry = opts.retry;
1293
+ this.base = (opts.gateway ?? DEFAULT_GATEWAY2).replace(/\/+$/, "");
893
1294
  this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
894
1295
  }
895
1296
  /** Full-text search. */
896
1297
  search(q, opts = {}) {
897
1298
  return this.query("/geocode/search", { q, ...toParams(opts) }, opts.signal);
898
1299
  }
1300
+ /**
1301
+ * Many searches in one or more `POST /geocode/batch` calls, chunked at
1302
+ * {@link GEOCODE_BATCH_MAX}. Results stay aligned to `queries`.
1303
+ */
1304
+ async batch(queries, opts = {}) {
1305
+ if (queries.length === 0) return [];
1306
+ const out = [];
1307
+ for (let i = 0; i < queries.length; i += GEOCODE_BATCH_MAX) {
1308
+ const chunk = queries.slice(i, i + GEOCODE_BATCH_MAX);
1309
+ const body = await this.postJson(
1310
+ "/geocode/batch",
1311
+ {
1312
+ queries: chunk,
1313
+ ...toBatchBody(opts)
1314
+ },
1315
+ opts.signal
1316
+ );
1317
+ if (!Array.isArray(body)) return out.concat(chunk.map(() => []));
1318
+ out.push(...body);
1319
+ }
1320
+ return out;
1321
+ }
899
1322
  /** Type-ahead suggestions for partial input. */
900
1323
  autocomplete(q, opts = {}) {
901
1324
  return this.query("/geocode/autocomplete", { q, ...toParams(opts) }, opts.signal);
@@ -930,7 +1353,9 @@ var Geocoder = class {
930
1353
  * Category search: POIs of a kind near a point, sorted by distance (metres, in each
931
1354
  * result's `distance`). `category` takes canonical taxonomy ids ("health.pharmacy") or
932
1355
  * plain words in English or French ("pharmacy", "pharmacie") — the gateway resolves
933
- * aliases, so this client passes the value through untouched.
1356
+ * aliases, so this client passes the value through untouched. An anchor outside Canada rejects
1357
+ * with status 422 and code `outside_coverage`; the corpus is not queried. A longitude or
1358
+ * latitude outside WGS84, including a bbox corner, rejects with status 400 and code `bad_request`.
934
1359
  */
935
1360
  nearby(category, opts = {}) {
936
1361
  return this.query(
@@ -946,11 +1371,21 @@ var Geocoder = class {
946
1371
  opts.signal
947
1372
  );
948
1373
  }
949
- /** Reverse geocode a coordinate. */
1374
+ /**
1375
+ * Reverse geocode a coordinate. A point outside Canada rejects with status 422 and code
1376
+ * `outside_coverage`. A longitude or latitude outside WGS84 rejects with status 400 and code
1377
+ * `bad_request`.
1378
+ */
950
1379
  reverse(lng, lat, opts = {}) {
951
1380
  return this.query(
952
1381
  "/geocode/reverse",
953
- { lon: String(lng), lat: String(lat), lang: opts.lang },
1382
+ {
1383
+ lon: String(lng),
1384
+ lat: String(lat),
1385
+ lang: opts.lang,
1386
+ limit: opts.limit != null ? String(opts.limit) : void 0,
1387
+ ...treatyParams(opts)
1388
+ },
954
1389
  opts.signal
955
1390
  );
956
1391
  }
@@ -990,14 +1425,27 @@ var Geocoder = class {
990
1425
  opts.signal
991
1426
  );
992
1427
  }
993
- async requestJson(path, params, signal) {
1428
+ async postJson(path, body, signal) {
994
1429
  const q = new URLSearchParams({ key: this.key });
995
- for (const [k, v] of Object.entries(params)) if (v != null && v !== "") q.set(k, v);
996
1430
  const res = await this.fetchImpl(`${this.base}${path}?${q}`, {
1431
+ method: "POST",
997
1432
  signal,
998
- headers: { accept: "application/json" }
1433
+ headers: { accept: "application/json", "content-type": "application/json" },
1434
+ body: JSON.stringify(body)
999
1435
  });
1000
- if (!res.ok) throw await errorFrom(res, path);
1436
+ if (!res.ok) throw await errorFrom2(res, path);
1437
+ return res.json();
1438
+ }
1439
+ async requestJson(path, params, signal) {
1440
+ const q = new URLSearchParams({ key: this.key });
1441
+ for (const [k, v] of Object.entries(params)) if (v != null && v !== "") q.set(k, v);
1442
+ const res = await fetchWithRetry2(
1443
+ this.fetchImpl,
1444
+ `${this.base}${path}?${q}`,
1445
+ { signal, headers: { accept: "application/json" } },
1446
+ this.retry
1447
+ );
1448
+ if (!res.ok) throw await errorFrom2(res, path);
1001
1449
  return res.json();
1002
1450
  }
1003
1451
  async query(path, params, signal) {
@@ -1005,6 +1453,20 @@ var Geocoder = class {
1005
1453
  return Array.isArray(body) ? body : body.results ?? [];
1006
1454
  }
1007
1455
  };
1456
+ function toBatchBody(o) {
1457
+ return {
1458
+ ...o.limit != null ? { limit: o.limit } : {},
1459
+ ...o.lang ? { lang: o.lang } : {},
1460
+ ...o.bbox ? { bbox: o.bbox.join(",") } : {},
1461
+ ...o.focus ? { focus: o.focus.join(",") } : {},
1462
+ ...o.region ? { region: o.region } : {},
1463
+ ...o.layers ? { layers: o.layers } : {}
1464
+ };
1465
+ }
1466
+ function treatyParams(o) {
1467
+ const include = o.include && o.include.length > 0 ? [...new Set(o.include)].join(",") : void 0;
1468
+ return { include, treaties_version: o.treatiesVersion };
1469
+ }
1008
1470
  function toParams(o) {
1009
1471
  return {
1010
1472
  limit: o.limit != null ? String(o.limit) : void 0,
@@ -1013,32 +1475,42 @@ function toParams(o) {
1013
1475
  focus: o.focus ? o.focus.join(",") : void 0,
1014
1476
  region: o.region,
1015
1477
  geometry: o.geometry ? "true" : void 0,
1016
- layers: o.layers
1478
+ layers: o.layers,
1479
+ ...treatyParams(o)
1017
1480
  };
1018
1481
  }
1019
1482
  export {
1020
1483
  AMBIGUOUS_SCORE_RATIO,
1021
1484
  CIVIC_SEARCH_LAYERS,
1485
+ COMMUNITY_INCLUDE_VALUES,
1022
1486
  CsvParser,
1023
1487
  DEFAULT_IDENTIFY_LAYERS,
1024
1488
  DEFAULT_QUERY_LIMIT,
1025
1489
  ENERGY_IDENTIFY_LAYER_IDS,
1026
1490
  ENERGY_SEARCH_LAYERS,
1491
+ GEOCODE_BATCH_MAX,
1027
1492
  Geocoder,
1028
1493
  GeocoderError,
1029
1494
  IDENTIFY_LAYER_IDS,
1495
+ INCLUDE_VALUES,
1030
1496
  LAND_IDENTIFY_LAYER_IDS,
1031
1497
  MAX_QUERY_LIMIT,
1032
1498
  MINING_IDENTIFY_LAYER_IDS,
1033
1499
  OUTPUT_COLUMNS,
1034
1500
  OUTPUT_PREFIX,
1501
+ Places,
1502
+ PlacesError,
1035
1503
  QUERY_LAYER_IDS,
1036
1504
  REGIONS,
1037
1505
  REGION_CODES,
1038
1506
  REGION_KEYS,
1039
1507
  STRUCTURED_FIELDS,
1508
+ UnmapError3 as UnmapError,
1040
1509
  alignRow,
1041
1510
  classify,
1511
+ createTreatyOverlay,
1512
+ emptyAcknowledgementContext,
1513
+ emptyTerritoryContext,
1042
1514
  encodeField,
1043
1515
  encodeRow,
1044
1516
  energySearchRequested,
@@ -1047,6 +1519,7 @@ export {
1047
1519
  isEnergySearchSource,
1048
1520
  isIdentifyLayer,
1049
1521
  isLandIdentifyLayer,
1522
+ isTreatiesVersion,
1050
1523
  knownIdentifyLayers,
1051
1524
  knownQueryLayers,
1052
1525
  knownSearchLayers,
@@ -1055,23 +1528,34 @@ export {
1055
1528
  looksLikeLegalLand,
1056
1529
  looksLikeParcel,
1057
1530
  looksLikeRuralRoute,
1531
+ orderTreatyMatches,
1058
1532
  outputValues,
1533
+ parseAcknowledgementContext,
1059
1534
  parseEnergyQuery,
1060
1535
  parseEnergySearchLayers,
1061
1536
  parseFocus,
1062
1537
  parseIdentifyAt,
1063
1538
  parseIdentifyLayers,
1539
+ parseInclude,
1064
1540
  parseLegalLand,
1065
1541
  parseParcel,
1066
1542
  parseQueryBbox,
1067
1543
  parseQueryLayer,
1068
1544
  parseQueryLimit,
1069
1545
  parseRegion,
1546
+ parseRetryAfter3 as parseRetryAfter,
1070
1547
  parseRuralRoute,
1071
1548
  parseSearchLayers,
1072
1549
  parseStructuredQuery,
1550
+ parseTerritoryContext,
1551
+ parseTreatiesVersion,
1552
+ parseTreatyContext,
1553
+ pickCommunityName,
1554
+ pickTreatyLabel,
1073
1555
  regionForms,
1074
1556
  resolveOutputHeader,
1075
- toLegalLandDetails
1557
+ toLegalLandDetails,
1558
+ treatyLayerIds,
1559
+ unavailableTreatyContext
1076
1560
  };
1077
1561
  //# sourceMappingURL=index.js.map