@unmap/geocoding 0.10.0 → 0.13.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 +14 -4
- package/dist/index.d.ts +801 -3
- package/dist/index.js +1078 -7
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,951 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
+
import { DEFAULT_GATEWAY as DEFAULT_GATEWAY2 } from "@unmap/core";
|
|
3
|
+
|
|
4
|
+
// src/legal-land.ts
|
|
5
|
+
var SEP = String.raw`(?:\s*-\s*|\s+)`;
|
|
6
|
+
var SEP_OPT = String.raw`(?:\s*-\s*|\s+)?`;
|
|
7
|
+
var LSD_N = String.raw`(?<lsd>0?[1-9]|1[0-6])`;
|
|
8
|
+
var SEC_N = String.raw`(?<section>(?:0?[1-9]|[1-2][0-9]|3[0-6])a?)`;
|
|
9
|
+
var TWP_N = String.raw`(?<township>(?:(?:00|0)?[1-9]|0?[1-9][0-9]|1[0-2][0-9])a?)`;
|
|
10
|
+
var RGE_N = String.raw`(?<range>(?:0?[1-9]|[1-8][0-9]|9[0-3])a?)`;
|
|
11
|
+
var MER_N = String.raw`(?<meridian>[Ww][1-6]|[Ee][1-2])[Mm]?`;
|
|
12
|
+
var DLS_LSD = new RegExp(`^${LSD_N}${SEP}${SEC_N}${SEP}${TWP_N}${SEP}${RGE_N}${SEP_OPT}${MER_N}$`, "i");
|
|
13
|
+
var DLS_SEC = new RegExp(`^${SEC_N}${SEP}${TWP_N}${SEP}${RGE_N}${SEP_OPT}${MER_N}$`, "i");
|
|
14
|
+
var DLS_TWP = new RegExp(`^${TWP_N}${SEP}${RGE_N}${SEP_OPT}${MER_N}$`, "i");
|
|
15
|
+
var DLS_QTR_MATCH = new RegExp(`^([ns][ew])(1/4)?(?:\\s*-\\s*|\\s+).*$`, "i");
|
|
16
|
+
var DLS_QTR = new RegExp(
|
|
17
|
+
`^(?<quarter>[ns][ew])(?:[\\s-]*1/4)?${SEP_OPT}${SEC_N}${SEP}${TWP_N}${SEP}${RGE_N}${SEP_OPT}${MER_N}$`,
|
|
18
|
+
"i"
|
|
19
|
+
);
|
|
20
|
+
var DLS_HALF_MATCH = new RegExp(`^([nsew])(1/2)?(?:\\s*-\\s*|\\s+).*$`, "i");
|
|
21
|
+
var DLS_HALF = new RegExp(
|
|
22
|
+
`^(?<half>[nsew])(?:[\\s-]*1/2)?${SEP_OPT}${SEC_N}${SEP}${TWP_N}${SEP}${RGE_N}${SEP_OPT}${MER_N}$`,
|
|
23
|
+
"i"
|
|
24
|
+
);
|
|
25
|
+
var NTS = new RegExp(
|
|
26
|
+
`^(?<quarterUnit>[abcd])${SEP_OPT}(?<ntsUnit>(?:00|0)?[1-9]|0?[1-9][0-9]|100)${SEP_OPT}(?<block>[a-l])\\s*/?\\s*(?<series>0?8[2-9]|0?9[0-9]|10[0-9]|11[0-4])${SEP_OPT}(?<mapArea>[a-p])${SEP_OPT}(?<mapSheet>0?[1-9]|1[0-6])$`,
|
|
27
|
+
"i"
|
|
28
|
+
);
|
|
29
|
+
var NTS_SHEET = new RegExp(
|
|
30
|
+
`^(?<series>0?8[2-9]|0?9[0-9]|10[0-9]|11[0-4])(?<mapArea>[a-p])\\s*/?\\s*(?<mapSheet>0?[1-9]|1[0-6])$`,
|
|
31
|
+
"i"
|
|
32
|
+
);
|
|
33
|
+
var FPS_UNIT = /^([a-p])\s+(\d{1,2})\s+(\d{2})-(\d{2})\s+n\s+(\d{1,3})-(\d{1,3})\s+w$/i;
|
|
34
|
+
var FPS_SECTION = /^(\d{1,2})\s+(\d{2})-(\d{2})\s+n\s+(\d{1,3})-(\d{1,3})\s+w$/i;
|
|
35
|
+
var FPS_GRID = /^(\d{2})-(\d{2})\s+n\s+(\d{1,3})-(\d{1,3})\s+w$/i;
|
|
36
|
+
var ON_LOT = /^lot\s+(\d+)(?:\s+(.*))?$/i;
|
|
37
|
+
var ON_CON = /^(?:con|concession)\s+(\d+)\s+([\w ]+)$/i;
|
|
38
|
+
var ON_CON_IN_REST = /^(?:con|concession)\s+(\d+)\s*(.*)$/i;
|
|
39
|
+
var ON_LOT_IN_REST = /^lot\s+(\d+)\s*(.*)$/i;
|
|
40
|
+
var RIVER = /^(pl|rl|wl|lx|sl|ot)(?:[\s-]+[a-z0-9]+)+$/i;
|
|
41
|
+
function sanitize(q) {
|
|
42
|
+
return q.trim().replace(/['"]/g, "").replace(/[;\\]/g, "");
|
|
43
|
+
}
|
|
44
|
+
function insertDirectionSeparator(q) {
|
|
45
|
+
return q.replace(/^([NS][EW]|[NSEW])(?!1\/[24]\b)(\d)/i, "$1-$2");
|
|
46
|
+
}
|
|
47
|
+
function cleanPart(value) {
|
|
48
|
+
return value.replace(/^0+(?=\d)/, "").replace(/\/0+/, "/").toUpperCase();
|
|
49
|
+
}
|
|
50
|
+
function dlsProvince(meridian) {
|
|
51
|
+
const m = meridian.toUpperCase();
|
|
52
|
+
if (m === "W5") return "AB";
|
|
53
|
+
if (m === "W1" || m === "E1" || m === "E2") return "MB";
|
|
54
|
+
return void 0;
|
|
55
|
+
}
|
|
56
|
+
function titleCase(value) {
|
|
57
|
+
return value.replace(/\w\S*/g, (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
|
|
58
|
+
}
|
|
59
|
+
function groups(re, q) {
|
|
60
|
+
const match = re.exec(q);
|
|
61
|
+
if (!match?.groups) return null;
|
|
62
|
+
const out = {};
|
|
63
|
+
for (const [key, value] of Object.entries(match.groups)) {
|
|
64
|
+
if (value === void 0 || value === "") continue;
|
|
65
|
+
out[key] = cleanPart(value);
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
function dlsFrom(unit, g) {
|
|
70
|
+
const meridian = g.meridian;
|
|
71
|
+
const components = { meridian, range: g.range, township: g.township };
|
|
72
|
+
const pieces = [];
|
|
73
|
+
if (unit === "lsd") {
|
|
74
|
+
components.lsd = g.lsd;
|
|
75
|
+
components.section = g.section;
|
|
76
|
+
pieces.push(g.lsd, g.section);
|
|
77
|
+
} else if (unit === "qtr") {
|
|
78
|
+
components.quarter = g.quarter;
|
|
79
|
+
components.section = g.section;
|
|
80
|
+
pieces.push(g.quarter, g.section);
|
|
81
|
+
} else if (unit === "half") {
|
|
82
|
+
components.half = g.half;
|
|
83
|
+
components.section = g.section;
|
|
84
|
+
pieces.push(g.half, g.section);
|
|
85
|
+
} else if (unit === "sec") {
|
|
86
|
+
components.section = g.section;
|
|
87
|
+
pieces.push(g.section);
|
|
88
|
+
}
|
|
89
|
+
pieces.push(g.township, g.range, meridian);
|
|
90
|
+
const parsed = {
|
|
91
|
+
system: "DLS",
|
|
92
|
+
canonical: pieces.join("-"),
|
|
93
|
+
unit,
|
|
94
|
+
components
|
|
95
|
+
};
|
|
96
|
+
const province = dlsProvince(meridian);
|
|
97
|
+
if (province) parsed.province = province;
|
|
98
|
+
return parsed;
|
|
99
|
+
}
|
|
100
|
+
function parseDls(q) {
|
|
101
|
+
const candidate = insertDirectionSeparator(q);
|
|
102
|
+
const lsd = groups(DLS_LSD, candidate);
|
|
103
|
+
if (lsd) return dlsFrom("lsd", lsd);
|
|
104
|
+
if (DLS_QTR_MATCH.test(candidate)) {
|
|
105
|
+
const qtr = groups(DLS_QTR, candidate);
|
|
106
|
+
if (qtr) return dlsFrom("qtr", qtr);
|
|
107
|
+
}
|
|
108
|
+
if (DLS_HALF_MATCH.test(candidate)) {
|
|
109
|
+
const half = groups(DLS_HALF, candidate);
|
|
110
|
+
if (half) return dlsFrom("half", half);
|
|
111
|
+
}
|
|
112
|
+
const sec = groups(DLS_SEC, candidate);
|
|
113
|
+
if (sec) return dlsFrom("sec", sec);
|
|
114
|
+
const twp = groups(DLS_TWP, candidate);
|
|
115
|
+
if (twp) return dlsFrom("twp", twp);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function parseNts(q) {
|
|
119
|
+
const full = groups(NTS, q);
|
|
120
|
+
if (full) {
|
|
121
|
+
return {
|
|
122
|
+
system: "NTS",
|
|
123
|
+
canonical: `${full.quarterUnit}-${full.ntsUnit}-${full.block}/${full.series}-${full.mapArea}-${full.mapSheet}`,
|
|
124
|
+
unit: "nts",
|
|
125
|
+
components: {
|
|
126
|
+
quarterUnit: full.quarterUnit,
|
|
127
|
+
ntsUnit: full.ntsUnit,
|
|
128
|
+
block: full.block,
|
|
129
|
+
series: full.series,
|
|
130
|
+
mapArea: full.mapArea,
|
|
131
|
+
mapSheet: full.mapSheet
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const sheet = groups(NTS_SHEET, q);
|
|
136
|
+
if (!sheet) return null;
|
|
137
|
+
return {
|
|
138
|
+
system: "NTS",
|
|
139
|
+
canonical: `${sheet.series}${sheet.mapArea}/${sheet.mapSheet}`,
|
|
140
|
+
unit: "nts_sheet",
|
|
141
|
+
components: {
|
|
142
|
+
series: sheet.series,
|
|
143
|
+
mapArea: sheet.mapArea,
|
|
144
|
+
mapSheet: sheet.mapSheet
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function parseFps(q) {
|
|
149
|
+
const unit = FPS_UNIT.exec(q);
|
|
150
|
+
if (unit) {
|
|
151
|
+
const components2 = {
|
|
152
|
+
fpsUnit: unit[1].toUpperCase(),
|
|
153
|
+
fpsSection: unit[2],
|
|
154
|
+
latDeg: unit[3],
|
|
155
|
+
latMin: unit[4],
|
|
156
|
+
lonDeg: unit[5],
|
|
157
|
+
lonMin: unit[6]
|
|
158
|
+
};
|
|
159
|
+
return {
|
|
160
|
+
system: "FPS",
|
|
161
|
+
canonical: `${components2.fpsUnit} ${components2.fpsSection} ${components2.latDeg}-${components2.latMin} N ${components2.lonDeg}-${components2.lonMin} W`,
|
|
162
|
+
unit: "unit",
|
|
163
|
+
components: components2
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const section = FPS_SECTION.exec(q);
|
|
167
|
+
if (section) {
|
|
168
|
+
const components2 = {
|
|
169
|
+
fpsSection: section[1],
|
|
170
|
+
latDeg: section[2],
|
|
171
|
+
latMin: section[3],
|
|
172
|
+
lonDeg: section[4],
|
|
173
|
+
lonMin: section[5]
|
|
174
|
+
};
|
|
175
|
+
return {
|
|
176
|
+
system: "FPS",
|
|
177
|
+
canonical: `${components2.fpsSection} ${components2.latDeg}-${components2.latMin} N ${components2.lonDeg}-${components2.lonMin} W`,
|
|
178
|
+
unit: "section",
|
|
179
|
+
components: components2
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const grid = FPS_GRID.exec(q);
|
|
183
|
+
if (!grid) return null;
|
|
184
|
+
const components = {
|
|
185
|
+
latDeg: grid[1],
|
|
186
|
+
latMin: grid[2],
|
|
187
|
+
lonDeg: grid[3],
|
|
188
|
+
lonMin: grid[4]
|
|
189
|
+
};
|
|
190
|
+
return {
|
|
191
|
+
system: "FPS",
|
|
192
|
+
canonical: `${components.latDeg}-${components.latMin} N ${components.lonDeg}-${components.lonMin} W`,
|
|
193
|
+
unit: "grid",
|
|
194
|
+
components
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function parseGts(q) {
|
|
198
|
+
const lot = ON_LOT.exec(q);
|
|
199
|
+
if (lot) {
|
|
200
|
+
const components2 = { lot: cleanPart(lot[1]) };
|
|
201
|
+
const rest2 = (lot[2] ?? "").trim();
|
|
202
|
+
const con2 = ON_CON_IN_REST.exec(rest2);
|
|
203
|
+
const nameBits = [];
|
|
204
|
+
if (con2) {
|
|
205
|
+
components2.concession = cleanPart(con2[1]);
|
|
206
|
+
const after = (con2[2] ?? "").trim();
|
|
207
|
+
if (after) components2.townshipName = titleCase(after);
|
|
208
|
+
nameBits.push(`Con ${components2.concession}`);
|
|
209
|
+
if (components2.townshipName) nameBits.push(components2.townshipName);
|
|
210
|
+
} else if (rest2) {
|
|
211
|
+
components2.townshipName = titleCase(rest2);
|
|
212
|
+
nameBits.push(components2.townshipName);
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
system: "GTS",
|
|
216
|
+
canonical: [`Lot ${components2.lot}`, ...nameBits].join(" "),
|
|
217
|
+
unit: "lot",
|
|
218
|
+
components: components2,
|
|
219
|
+
province: "ON"
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const con = ON_CON.exec(q);
|
|
223
|
+
if (!con) return null;
|
|
224
|
+
const components = { concession: cleanPart(con[1]) };
|
|
225
|
+
const rest = con[2].trim();
|
|
226
|
+
const nestedLot = ON_LOT_IN_REST.exec(rest);
|
|
227
|
+
if (nestedLot) {
|
|
228
|
+
components.lot = cleanPart(nestedLot[1]);
|
|
229
|
+
const after = (nestedLot[2] ?? "").trim();
|
|
230
|
+
if (after) components.townshipName = titleCase(after);
|
|
231
|
+
} else {
|
|
232
|
+
components.townshipName = titleCase(rest);
|
|
233
|
+
}
|
|
234
|
+
const tail = [
|
|
235
|
+
components.lot ? `Lot ${components.lot}` : "",
|
|
236
|
+
components.townshipName ?? ""
|
|
237
|
+
].filter(Boolean).join(" ");
|
|
238
|
+
return {
|
|
239
|
+
system: "GTS",
|
|
240
|
+
canonical: [`Con ${components.concession}`, tail].filter(Boolean).join(" "),
|
|
241
|
+
unit: components.lot ? "lot" : "con",
|
|
242
|
+
components,
|
|
243
|
+
province: "ON"
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function parseRiverLot(q) {
|
|
247
|
+
if (!RIVER.test(q)) return null;
|
|
248
|
+
const tokens = q.split(/[\s-]+/).filter(Boolean).map((t) => t.toUpperCase());
|
|
249
|
+
const kind = tokens[0];
|
|
250
|
+
const parts = tokens.slice(1);
|
|
251
|
+
return {
|
|
252
|
+
system: "RIVER_LOT",
|
|
253
|
+
canonical: tokens.join("-"),
|
|
254
|
+
unit: "river_lot",
|
|
255
|
+
components: { kind, parts },
|
|
256
|
+
province: "MB"
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function parseLegalLand(q) {
|
|
260
|
+
if (typeof q !== "string") return null;
|
|
261
|
+
const raw = sanitize(q);
|
|
262
|
+
if (!raw) return null;
|
|
263
|
+
return parseFps(raw) ?? parseGts(raw) ?? parseRiverLot(raw) ?? parseDls(raw) ?? parseNts(raw);
|
|
264
|
+
}
|
|
265
|
+
var LAYER_SYSTEM = {
|
|
266
|
+
DLS: "dls",
|
|
267
|
+
NTS: "nts",
|
|
268
|
+
GTS: "on",
|
|
269
|
+
FPS: "fps",
|
|
270
|
+
RIVER_LOT: "river_lot"
|
|
271
|
+
};
|
|
272
|
+
function looksLikeLegalLand(q) {
|
|
273
|
+
return parseLegalLand(q) != null;
|
|
274
|
+
}
|
|
275
|
+
function toLegalLandDetails(parsed) {
|
|
276
|
+
return {
|
|
277
|
+
system: LAYER_SYSTEM[parsed.system],
|
|
278
|
+
...parsed.province ? { province: parsed.province } : {},
|
|
279
|
+
canonical: parsed.canonical,
|
|
280
|
+
unit: parsed.unit,
|
|
281
|
+
components: parsed.components
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/energy.ts
|
|
286
|
+
var ENERGY_SEARCH_LAYERS = ["well", "facility"];
|
|
287
|
+
var DLS_UWI = /^\s*(?:[A-Z0-9]{2}|\d{3})\/(1[0-6]|0?[1-9])-(3[0-6]|[12][0-9]|0?[1-9])-(1[0-2][0-9]|0?[1-9][0-9]|00[1-9])-(([1-8][0-9]|9[0-3]|0?[1-9])\s*(w[1-6]|e[1-2]|[1-6]))\s*m?\/?(\w{1,2})?\s*$/i;
|
|
288
|
+
var BC_UWI = /^\s*\d{3}\/(a|b|c|d)(\s*-\s*|\s+)?((00|0)?[1-9]|0?[1-9][0-9]|100)(\s*-\s*|\s+)?(a|b|c|d|e|f|g|h|i|j|k|l)\s*?\/\s*(0?8[2-9]|0?9[0-9]|10[0-9]|11[0-4])(\s*-\s*|\s+)?(a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p)(\s*-\s*|\s+)?(1[0-6]|0?[1-9])\/\d{1,2}\s*$/i;
|
|
289
|
+
var WELL_LICENCE = /^\s*(?:w|wl|lic(?:\.|ence|ense)?)\s*[-#]?\s*(\d{5,7})\s*$/i;
|
|
290
|
+
function parseEnergyQuery(q) {
|
|
291
|
+
if (typeof q !== "string") return null;
|
|
292
|
+
const raw = q.trim();
|
|
293
|
+
if (!raw) return null;
|
|
294
|
+
if (BC_UWI.test(raw) || DLS_UWI.test(raw)) {
|
|
295
|
+
return { kind: "uwi", raw, key: raw.replace(/\s+/g, "").toUpperCase() };
|
|
296
|
+
}
|
|
297
|
+
const licence = WELL_LICENCE.exec(raw);
|
|
298
|
+
if (licence?.[1]) {
|
|
299
|
+
return { kind: "licence", raw, key: licence[1] };
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
function looksLikeEnergyQuery(q) {
|
|
304
|
+
return parseEnergyQuery(q) != null;
|
|
305
|
+
}
|
|
306
|
+
function parseEnergySearchLayers(raw) {
|
|
307
|
+
if (!raw) return [];
|
|
308
|
+
const out = [];
|
|
309
|
+
const seen = /* @__PURE__ */ new Set();
|
|
310
|
+
for (const token of raw.split(",")) {
|
|
311
|
+
const id = token.trim();
|
|
312
|
+
if (id === "well" || id === "facility") {
|
|
313
|
+
if (!seen.has(id)) {
|
|
314
|
+
seen.add(id);
|
|
315
|
+
out.push(id);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
function isEnergySearchSource(raw) {
|
|
322
|
+
return raw?.trim() === "energy";
|
|
323
|
+
}
|
|
324
|
+
function energySearchRequested(q, opts = {}) {
|
|
325
|
+
if (looksLikeEnergyQuery(q)) return true;
|
|
326
|
+
if (parseEnergySearchLayers(opts.layers).length > 0) return true;
|
|
327
|
+
return isEnergySearchSource(opts.profile) || isEnergySearchSource(opts.source);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/identify.ts
|
|
331
|
+
var LAND_IDENTIFY_LAYER_IDS = [
|
|
332
|
+
"municipalities",
|
|
333
|
+
"survey.dls",
|
|
334
|
+
"survey.nts",
|
|
335
|
+
"survey.parcels"
|
|
336
|
+
];
|
|
337
|
+
var ENERGY_IDENTIFY_LAYER_IDS = [
|
|
338
|
+
"energy.wells",
|
|
339
|
+
"energy.pipelines",
|
|
340
|
+
"energy.facilities"
|
|
341
|
+
];
|
|
342
|
+
var MINING_IDENTIFY_LAYER_IDS = [
|
|
343
|
+
"mining.occurrences",
|
|
344
|
+
"mining.prospectivity",
|
|
345
|
+
"mining.sk-potential"
|
|
346
|
+
];
|
|
347
|
+
var IDENTIFY_LAYER_IDS = [
|
|
348
|
+
...LAND_IDENTIFY_LAYER_IDS,
|
|
349
|
+
...ENERGY_IDENTIFY_LAYER_IDS,
|
|
350
|
+
"energy.fields",
|
|
351
|
+
"energy.petroleum-tenure",
|
|
352
|
+
"energy.mineral-tenure",
|
|
353
|
+
...MINING_IDENTIFY_LAYER_IDS
|
|
354
|
+
];
|
|
355
|
+
var DEFAULT_IDENTIFY_LAYERS = ["municipalities"];
|
|
356
|
+
function isIdentifyLayer(id) {
|
|
357
|
+
return IDENTIFY_LAYER_IDS.includes(id);
|
|
358
|
+
}
|
|
359
|
+
function isLandIdentifyLayer(id) {
|
|
360
|
+
return LAND_IDENTIFY_LAYER_IDS.includes(id) || MINING_IDENTIFY_LAYER_IDS.includes(id);
|
|
361
|
+
}
|
|
362
|
+
function isEnergyIdentifyLayer(id) {
|
|
363
|
+
return id.startsWith("energy.");
|
|
364
|
+
}
|
|
365
|
+
function knownIdentifyLayers() {
|
|
366
|
+
return IDENTIFY_LAYER_IDS.join(", ");
|
|
367
|
+
}
|
|
368
|
+
function parseIdentifyLayers(raw) {
|
|
369
|
+
const ids = (raw ?? "").split(",").map((id) => id.trim()).filter((id) => id.length > 0);
|
|
370
|
+
if (ids.length === 0) {
|
|
371
|
+
return { layers: [...DEFAULT_IDENTIFY_LAYERS] };
|
|
372
|
+
}
|
|
373
|
+
const layers = [];
|
|
374
|
+
const seen = /* @__PURE__ */ new Set();
|
|
375
|
+
for (const id of ids) {
|
|
376
|
+
if (!isIdentifyLayer(id)) {
|
|
377
|
+
return { error: `unknown identify layer "${id}". known: ${knownIdentifyLayers()}` };
|
|
378
|
+
}
|
|
379
|
+
if (!seen.has(id)) {
|
|
380
|
+
seen.add(id);
|
|
381
|
+
layers.push(id);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return { layers };
|
|
385
|
+
}
|
|
386
|
+
function parseIdentifyAt(at, lon, lat) {
|
|
387
|
+
const rawAt = at?.trim();
|
|
388
|
+
if (rawAt) {
|
|
389
|
+
const pieces = rawAt.split(",");
|
|
390
|
+
if (pieces.length !== 2 || pieces.some((p) => p.trim() === "")) {
|
|
391
|
+
return { error: "at must be lng,lat" };
|
|
392
|
+
}
|
|
393
|
+
const lng = Number(pieces[0]);
|
|
394
|
+
const latN = Number(pieces[1]);
|
|
395
|
+
if (!Number.isFinite(lng) || !Number.isFinite(latN)) {
|
|
396
|
+
return { error: "at must be lng,lat" };
|
|
397
|
+
}
|
|
398
|
+
return { at: [lng, latN] };
|
|
399
|
+
}
|
|
400
|
+
if (lon != null && lon !== "" && lat != null && lat !== "") {
|
|
401
|
+
const lng = Number(lon);
|
|
402
|
+
const latN = Number(lat);
|
|
403
|
+
if (!Number.isFinite(lng) || !Number.isFinite(latN)) {
|
|
404
|
+
return { error: "lon and lat must be numbers" };
|
|
405
|
+
}
|
|
406
|
+
return { at: [lng, latN] };
|
|
407
|
+
}
|
|
408
|
+
return { error: "at (lng,lat) or lon and lat required" };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/batch/csv.ts
|
|
412
|
+
var QUOTE = 34;
|
|
413
|
+
var COMMA = 44;
|
|
414
|
+
var CR = 13;
|
|
415
|
+
var LF = 10;
|
|
416
|
+
var CsvParser = class {
|
|
417
|
+
field = "";
|
|
418
|
+
row = [];
|
|
419
|
+
inQuotes = false;
|
|
420
|
+
/** A quote seen inside a quoted field: either an escape or the close. */
|
|
421
|
+
pendingQuote = false;
|
|
422
|
+
/** A CR seen outside quotes: end of record, and a following LF is part of the same break. */
|
|
423
|
+
pendingCr = false;
|
|
424
|
+
started = false;
|
|
425
|
+
rowHasContent = false;
|
|
426
|
+
/**
|
|
427
|
+
* Feed one chunk. Returns the records completed by it.
|
|
428
|
+
*
|
|
429
|
+
* A record that ends exactly at a chunk boundary is returned here; a partial one is held until
|
|
430
|
+
* the next chunk, or until `end()`.
|
|
431
|
+
*/
|
|
432
|
+
write(chunk) {
|
|
433
|
+
const out = [];
|
|
434
|
+
if (!this.started) {
|
|
435
|
+
this.started = true;
|
|
436
|
+
if (chunk.charCodeAt(0) === 65279) chunk = chunk.slice(1);
|
|
437
|
+
}
|
|
438
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
439
|
+
const ch = chunk.charCodeAt(i);
|
|
440
|
+
if (this.pendingCr) {
|
|
441
|
+
this.pendingCr = false;
|
|
442
|
+
if (ch === LF) continue;
|
|
443
|
+
}
|
|
444
|
+
if (this.inQuotes) {
|
|
445
|
+
if (this.pendingQuote) {
|
|
446
|
+
this.pendingQuote = false;
|
|
447
|
+
if (ch === QUOTE) {
|
|
448
|
+
this.field += '"';
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
this.inQuotes = false;
|
|
452
|
+
} else if (ch === QUOTE) {
|
|
453
|
+
this.pendingQuote = true;
|
|
454
|
+
continue;
|
|
455
|
+
} else {
|
|
456
|
+
this.field += chunk[i];
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (ch === QUOTE && this.field === "") {
|
|
461
|
+
this.inQuotes = true;
|
|
462
|
+
this.rowHasContent = true;
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (ch === COMMA) {
|
|
466
|
+
this.row.push(this.field);
|
|
467
|
+
this.field = "";
|
|
468
|
+
this.rowHasContent = true;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (ch === CR) {
|
|
472
|
+
this.pendingCr = true;
|
|
473
|
+
out.push(this.finishRow());
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
if (ch === LF) {
|
|
477
|
+
out.push(this.finishRow());
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
this.field += chunk[i];
|
|
481
|
+
this.rowHasContent = true;
|
|
482
|
+
}
|
|
483
|
+
return out.filter((r) => r !== EMPTY);
|
|
484
|
+
}
|
|
485
|
+
/** Flush a final record with no trailing newline. */
|
|
486
|
+
end() {
|
|
487
|
+
if (this.inQuotes && this.pendingQuote) this.inQuotes = false;
|
|
488
|
+
if (this.field !== "" || this.row.length > 0 || this.rowHasContent) {
|
|
489
|
+
const row = this.finishRow();
|
|
490
|
+
return row === EMPTY ? [] : [row];
|
|
491
|
+
}
|
|
492
|
+
return [];
|
|
493
|
+
}
|
|
494
|
+
/** True when the stream ended inside an unterminated quoted field. */
|
|
495
|
+
get unterminated() {
|
|
496
|
+
return this.inQuotes && !this.pendingQuote;
|
|
497
|
+
}
|
|
498
|
+
finishRow() {
|
|
499
|
+
this.row.push(this.field);
|
|
500
|
+
const row = this.row;
|
|
501
|
+
this.field = "";
|
|
502
|
+
this.row = [];
|
|
503
|
+
const hadContent = this.rowHasContent;
|
|
504
|
+
this.rowHasContent = false;
|
|
505
|
+
if (!hadContent && row.length === 1 && row[0] === "") return EMPTY;
|
|
506
|
+
return row;
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
var EMPTY = [];
|
|
510
|
+
function alignRow(header, values, index) {
|
|
511
|
+
if (values.length === header.length) return { record: { index, values: [...values] } };
|
|
512
|
+
if (values.length < header.length) {
|
|
513
|
+
return {
|
|
514
|
+
record: { index, values: [...values, ...Array(header.length - values.length).fill("")] },
|
|
515
|
+
problem: {
|
|
516
|
+
index,
|
|
517
|
+
code: "too_few_fields",
|
|
518
|
+
message: `row ${index} has ${values.length} fields, header has ${header.length}`
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
return {
|
|
523
|
+
record: { index, values: [...values] },
|
|
524
|
+
problem: {
|
|
525
|
+
index,
|
|
526
|
+
code: "too_many_fields",
|
|
527
|
+
message: `row ${index} has ${values.length} fields, header has ${header.length}`
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
function encodeField(value) {
|
|
532
|
+
if (value === "") return "";
|
|
533
|
+
if (/[",\r\n\t]/.test(value)) return `"${value.replace(/"/g, '""')}"`;
|
|
534
|
+
return value;
|
|
535
|
+
}
|
|
536
|
+
function looksLikeFormula(value) {
|
|
537
|
+
return /^[=+\-@\t\r]/.test(value);
|
|
538
|
+
}
|
|
539
|
+
function escapeForSpreadsheet(value) {
|
|
540
|
+
return looksLikeFormula(value) ? ` ${value}` : value;
|
|
541
|
+
}
|
|
542
|
+
function encodeRow(values, opts = {}) {
|
|
543
|
+
const prepared = opts.spreadsheetSafe ? values.map(escapeForSpreadsheet) : values;
|
|
544
|
+
return prepared.map(encodeField).join(",") + "\r\n";
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/batch/output.ts
|
|
548
|
+
var OUTPUT_PREFIX = "unmap_";
|
|
549
|
+
var OUTPUT_COLUMNS = [
|
|
550
|
+
"unmap_status",
|
|
551
|
+
"unmap_id",
|
|
552
|
+
"unmap_name",
|
|
553
|
+
"unmap_lng",
|
|
554
|
+
"unmap_lat",
|
|
555
|
+
"unmap_layer",
|
|
556
|
+
"unmap_source",
|
|
557
|
+
"unmap_match_type",
|
|
558
|
+
"unmap_precision",
|
|
559
|
+
"unmap_review"
|
|
560
|
+
];
|
|
561
|
+
function resolveOutputHeader(inputHeader) {
|
|
562
|
+
const taken = new Set(inputHeader.map((h) => h.trim().toLowerCase()));
|
|
563
|
+
const renamed = [];
|
|
564
|
+
const header = [...inputHeader];
|
|
565
|
+
for (const column of OUTPUT_COLUMNS) {
|
|
566
|
+
let name = column;
|
|
567
|
+
if (taken.has(name.toLowerCase())) {
|
|
568
|
+
let n = 2;
|
|
569
|
+
while (taken.has(`${column}_${n}`.toLowerCase())) n++;
|
|
570
|
+
name = `${column}_${n}`;
|
|
571
|
+
renamed.push({ column, to: name });
|
|
572
|
+
}
|
|
573
|
+
taken.add(name.toLowerCase());
|
|
574
|
+
header.push(name);
|
|
575
|
+
}
|
|
576
|
+
return { header, renamed };
|
|
577
|
+
}
|
|
578
|
+
var AMBIGUOUS_SCORE_RATIO = 0.95;
|
|
579
|
+
function classify(results) {
|
|
580
|
+
if (results.length === 0) return { status: "no_match", reasons: ["no_match"] };
|
|
581
|
+
const [top, second] = results;
|
|
582
|
+
const reasons = [];
|
|
583
|
+
if (top.match_type === "fallback") reasons.push("fallback_match");
|
|
584
|
+
for (const r of top.match_reasons ?? []) reasons.push(r);
|
|
585
|
+
if (second && typeof top.score === "number" && typeof second.score === "number" && top.score > 0 && second.score / top.score >= AMBIGUOUS_SCORE_RATIO) {
|
|
586
|
+
reasons.push("ambiguous");
|
|
587
|
+
}
|
|
588
|
+
return { status: reasons.length ? "review" : "matched", reasons };
|
|
589
|
+
}
|
|
590
|
+
function outputValues(status, reasons, top) {
|
|
591
|
+
return [
|
|
592
|
+
status,
|
|
593
|
+
top?.id ?? "",
|
|
594
|
+
top?.name ?? "",
|
|
595
|
+
// Fixed precision rather than the float's own repr: a CSV of coordinates is read by humans
|
|
596
|
+
// and by spreadsheets, and 51.04000000000001 is neither. 7 dp is ~1 cm, well beyond what any
|
|
597
|
+
// source in the corpus actually resolves.
|
|
598
|
+
top ? trimCoordinate(top.lng) : "",
|
|
599
|
+
top ? trimCoordinate(top.lat) : "",
|
|
600
|
+
top?.layer ?? "",
|
|
601
|
+
top?.source ?? "",
|
|
602
|
+
top?.match_type ?? "",
|
|
603
|
+
top?.precision ?? "",
|
|
604
|
+
reasons.join(" ")
|
|
605
|
+
];
|
|
606
|
+
}
|
|
607
|
+
function trimCoordinate(n) {
|
|
608
|
+
return n.toFixed(7).replace(/\.?0+$/, "");
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// src/search-layers.ts
|
|
612
|
+
var CIVIC_SEARCH_LAYERS = ["address", "street", "locality", "region", "poi"];
|
|
613
|
+
var CIVIC = new Set(CIVIC_SEARCH_LAYERS);
|
|
614
|
+
var ENERGY = new Set(ENERGY_SEARCH_LAYERS);
|
|
615
|
+
function knownSearchLayers() {
|
|
616
|
+
return [...CIVIC_SEARCH_LAYERS, ...ENERGY_SEARCH_LAYERS].join(", ");
|
|
617
|
+
}
|
|
618
|
+
function parseSearchLayers(raw) {
|
|
619
|
+
if (raw == null || raw.trim() === "") return { kind: "none" };
|
|
620
|
+
const civic = [];
|
|
621
|
+
const energy = [];
|
|
622
|
+
const seen = /* @__PURE__ */ new Set();
|
|
623
|
+
for (const token of raw.split(",")) {
|
|
624
|
+
const id = token.trim();
|
|
625
|
+
if (id === "") continue;
|
|
626
|
+
if (seen.has(id)) continue;
|
|
627
|
+
seen.add(id);
|
|
628
|
+
if (CIVIC.has(id)) civic.push(id);
|
|
629
|
+
else if (ENERGY.has(id)) energy.push(id);
|
|
630
|
+
else return { error: `unknown layer "${id}". Known layers: ${knownSearchLayers()}` };
|
|
631
|
+
}
|
|
632
|
+
if (civic.length && energy.length) {
|
|
633
|
+
return {
|
|
634
|
+
error: `layers cannot mix civic and energy values: ${civic.join(", ")} filter results within the civic corpus, while ${energy.join(", ")} select the energy corpus instead. Ask for one or the other.`
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
if (energy.length) return { kind: "energy", layers: energy };
|
|
638
|
+
if (civic.length) return { kind: "civic", layers: civic };
|
|
639
|
+
return { kind: "none" };
|
|
640
|
+
}
|
|
641
|
+
function parseFocus(raw) {
|
|
642
|
+
if (raw == null || raw.trim() === "") return void 0;
|
|
643
|
+
const pieces = raw.split(",").map((s) => s.trim());
|
|
644
|
+
const parts = pieces.map((s) => s === "" ? Number.NaN : Number(s));
|
|
645
|
+
if (parts.length !== 2 || !parts.every(Number.isFinite)) {
|
|
646
|
+
return { error: "focus must be lng,lat with two finite numbers" };
|
|
647
|
+
}
|
|
648
|
+
const [lng, lat] = parts;
|
|
649
|
+
if (lng < -180 || lng > 180) return { error: `focus longitude ${lng} is outside [-180, 180]` };
|
|
650
|
+
if (lat < -90 || lat > 90) return { error: `focus latitude ${lat} is outside [-90, 90]` };
|
|
651
|
+
return { focus: [lng, lat] };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/regions.ts
|
|
655
|
+
var BASE = {
|
|
656
|
+
ab: "AB",
|
|
657
|
+
alberta: "AB",
|
|
658
|
+
alta: "AB",
|
|
659
|
+
bc: "BC",
|
|
660
|
+
"british columbia": "BC",
|
|
661
|
+
"colombie-britannique": "BC",
|
|
662
|
+
mb: "MB",
|
|
663
|
+
manitoba: "MB",
|
|
664
|
+
nb: "NB",
|
|
665
|
+
"new brunswick": "NB",
|
|
666
|
+
"nouveau-brunswick": "NB",
|
|
667
|
+
nl: "NL",
|
|
668
|
+
"newfoundland and labrador": "NL",
|
|
669
|
+
newfoundland: "NL",
|
|
670
|
+
"terre-neuve-et-labrador": "NL",
|
|
671
|
+
nfld: "NL",
|
|
672
|
+
ns: "NS",
|
|
673
|
+
"nova scotia": "NS",
|
|
674
|
+
"nouvelle-\xE9cosse": "NS",
|
|
675
|
+
"nouvelle-ecosse": "NS",
|
|
676
|
+
nt: "NT",
|
|
677
|
+
"northwest territories": "NT",
|
|
678
|
+
"territoires du nord-ouest": "NT",
|
|
679
|
+
nwt: "NT",
|
|
680
|
+
nu: "NU",
|
|
681
|
+
nunavut: "NU",
|
|
682
|
+
on: "ON",
|
|
683
|
+
ontario: "ON",
|
|
684
|
+
ont: "ON",
|
|
685
|
+
pe: "PE",
|
|
686
|
+
pei: "PE",
|
|
687
|
+
"prince edward island": "PE",
|
|
688
|
+
"\xEEle-du-prince-\xE9douard": "PE",
|
|
689
|
+
qc: "QC",
|
|
690
|
+
pq: "QC",
|
|
691
|
+
quebec: "QC",
|
|
692
|
+
qu\u00E9bec: "QC",
|
|
693
|
+
"que.": "QC",
|
|
694
|
+
"qu\xE9.": "QC",
|
|
695
|
+
sk: "SK",
|
|
696
|
+
saskatchewan: "SK",
|
|
697
|
+
sask: "SK",
|
|
698
|
+
yt: "YT",
|
|
699
|
+
yukon: "YT"
|
|
700
|
+
};
|
|
701
|
+
for (const [k, v] of Object.entries(BASE)) {
|
|
702
|
+
if (/^[a-z]{2}$/.test(k)) BASE[k[0] + "." + k[1] + "."] = v;
|
|
703
|
+
else if (k === "nwt" || k === "pei") BASE[k.split("").join(".") + "."] = v;
|
|
704
|
+
else if (/^[a-z]{3,4}$/.test(k)) BASE[k + "."] = v;
|
|
705
|
+
}
|
|
706
|
+
var REGIONS = BASE;
|
|
707
|
+
var REGION_KEYS = Object.keys(BASE).sort((a, b) => b.length - a.length);
|
|
708
|
+
var REGION_CODES = [...new Set(Object.values(BASE))].sort();
|
|
709
|
+
function parseRegion(raw) {
|
|
710
|
+
if (!raw) return void 0;
|
|
711
|
+
const key = raw.trim().toLowerCase();
|
|
712
|
+
if (REGIONS[key]) return REGIONS[key];
|
|
713
|
+
const unaccented = key.normalize("NFD").replace(/\p{M}/gu, "");
|
|
714
|
+
for (const [k, v] of Object.entries(REGIONS)) {
|
|
715
|
+
if (k.normalize("NFD").replace(/\p{M}/gu, "") === unaccented) return v;
|
|
716
|
+
}
|
|
717
|
+
return void 0;
|
|
718
|
+
}
|
|
719
|
+
function regionForms(code) {
|
|
720
|
+
const forms = /* @__PURE__ */ new Set([code.toUpperCase()]);
|
|
721
|
+
for (const [k, v] of Object.entries(REGIONS)) if (v === code) forms.add(k.toUpperCase());
|
|
722
|
+
return [...forms];
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// src/structured.ts
|
|
726
|
+
var STRUCTURED_FIELDS = ["address", "city", "region", "postalcode", "country"];
|
|
727
|
+
var ANCHOR_FIELDS = ["address", "city", "postalcode"];
|
|
728
|
+
var POSTCODE = /^[A-Z]\d[A-Z]\d[A-Z]\d$/;
|
|
729
|
+
function parseStructuredQuery(raw) {
|
|
730
|
+
const trim = (v) => {
|
|
731
|
+
const t = (v ?? "").trim();
|
|
732
|
+
return t === "" ? void 0 : t;
|
|
733
|
+
};
|
|
734
|
+
const country = trim(raw.country);
|
|
735
|
+
if (country && !/^(ca|can|canada)$/i.test(country)) {
|
|
736
|
+
return { error: `country must be CA: unmap covers Canada only (got "${country}")` };
|
|
737
|
+
}
|
|
738
|
+
let region;
|
|
739
|
+
const rawRegion = trim(raw.region);
|
|
740
|
+
if (rawRegion) {
|
|
741
|
+
region = parseRegion(rawRegion);
|
|
742
|
+
if (!region) {
|
|
743
|
+
return { error: `unknown region "${rawRegion}". Use a province or territory code: ${REGION_CODES.join(", ")}` };
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
let postalcode;
|
|
747
|
+
const rawPostal = trim(raw.postalcode);
|
|
748
|
+
if (rawPostal) {
|
|
749
|
+
postalcode = rawPostal.replace(/\s+/g, "").toUpperCase();
|
|
750
|
+
if (!POSTCODE.test(postalcode)) {
|
|
751
|
+
return { error: `postalcode must be a Canadian postal code such as M5H 2N2 (got "${rawPostal}")` };
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
const query = {
|
|
755
|
+
...trim(raw.address) ? { address: trim(raw.address) } : {},
|
|
756
|
+
...trim(raw.city) ? { city: trim(raw.city) } : {},
|
|
757
|
+
...region ? { region } : {},
|
|
758
|
+
...postalcode ? { postalcode } : {},
|
|
759
|
+
country: "CA"
|
|
760
|
+
};
|
|
761
|
+
if (!ANCHOR_FIELDS.some((f) => query[f] != null)) {
|
|
762
|
+
return {
|
|
763
|
+
error: `structured search needs at least one of ${ANCHOR_FIELDS.join(", ")}. region and country narrow a search; they cannot be one on their own.`
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
return { query };
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// src/parcel.ts
|
|
770
|
+
var PID = /^\d{3}-?\d{3}-?\d{3}$/;
|
|
771
|
+
function parseParcel(q) {
|
|
772
|
+
if (typeof q !== "string") return null;
|
|
773
|
+
const raw = q.trim();
|
|
774
|
+
if (!PID.test(raw)) return null;
|
|
775
|
+
const digits = raw.replace(/-/g, "");
|
|
776
|
+
return {
|
|
777
|
+
raw,
|
|
778
|
+
pid: `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6)}`
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
function looksLikeParcel(q) {
|
|
782
|
+
return parseParcel(q) != null;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// src/query.ts
|
|
786
|
+
var QUERY_LAYER_IDS = IDENTIFY_LAYER_IDS;
|
|
787
|
+
var DEFAULT_QUERY_LIMIT = 50;
|
|
788
|
+
var MAX_QUERY_LIMIT = 50;
|
|
789
|
+
function knownQueryLayers() {
|
|
790
|
+
return knownIdentifyLayers();
|
|
791
|
+
}
|
|
792
|
+
function parseQueryLayer(raw) {
|
|
793
|
+
const id = (raw ?? "").trim();
|
|
794
|
+
if (!id) return { error: "layer required" };
|
|
795
|
+
if (isIdentifyLayer(id)) return { layer: id };
|
|
796
|
+
return {
|
|
797
|
+
unknown: id,
|
|
798
|
+
error: `unknown query layer "${id}". queryable: ${knownQueryLayers()}`
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function parseQueryBbox(raw) {
|
|
802
|
+
const value = raw?.trim();
|
|
803
|
+
if (!value) return { error: "bbox (minlng,minlat,maxlng,maxlat) required" };
|
|
804
|
+
const pieces = value.split(",");
|
|
805
|
+
if (pieces.length !== 4 || pieces.some((piece) => piece.trim() === "")) {
|
|
806
|
+
return { error: "bbox must be minlng,minlat,maxlng,maxlat" };
|
|
807
|
+
}
|
|
808
|
+
const nums = pieces.map(Number);
|
|
809
|
+
if (nums.some((n) => !Number.isFinite(n))) {
|
|
810
|
+
return { error: "bbox must be minlng,minlat,maxlng,maxlat" };
|
|
811
|
+
}
|
|
812
|
+
const [minLng, minLat, maxLng, maxLat] = nums;
|
|
813
|
+
if (minLng >= maxLng || minLat >= maxLat) {
|
|
814
|
+
return { error: "bbox must be minlng,minlat,maxlng,maxlat" };
|
|
815
|
+
}
|
|
816
|
+
return { bbox: [minLng, minLat, maxLng, maxLat] };
|
|
817
|
+
}
|
|
818
|
+
function parseQueryLimit(raw) {
|
|
819
|
+
if (raw == null || raw === "") return DEFAULT_QUERY_LIMIT;
|
|
820
|
+
const n = Number(raw);
|
|
821
|
+
return Number.isFinite(n) ? Math.min(Math.max(Math.trunc(n), 1), MAX_QUERY_LIMIT) : DEFAULT_QUERY_LIMIT;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// src/rural-route.ts
|
|
825
|
+
var KINDS = [
|
|
826
|
+
{ pattern: /\b(?:R\.?\s?R\.?|RURAL\s+ROUTE)\s*#?\s*(\d{1,3})\b/i, kind: "rural-route", numbered: true },
|
|
827
|
+
{ pattern: /\b(?:S\.?\s?S\.?|SUBURBAN\s+SERVICE)\s*#?\s*(\d{1,3})\b/i, kind: "suburban-service", numbered: true },
|
|
828
|
+
{ pattern: /\b(?:M\.?\s?R\.?|MOBILE\s+ROUTE)\s*#?\s*(\d{1,3})\b/i, kind: "mobile-route", numbered: true },
|
|
829
|
+
{ pattern: /\b(?:G\.?\s?D\.?|GENERAL\s+DELIVERY)\b/i, kind: "general-delivery", numbered: false }
|
|
830
|
+
];
|
|
831
|
+
var PARTS = [
|
|
832
|
+
{ pattern: /\bSITE\s*#?\s*([A-Z0-9-]{1,8})\b/i, field: "site" },
|
|
833
|
+
{ pattern: /\bCOMP(?:ARTMENT)?\s*#?\s*([A-Z0-9-]{1,8})\b/i, field: "compartment" },
|
|
834
|
+
{ pattern: /\b(?:PO\s*)?BOX\s*#?\s*([A-Z0-9-]{1,8})\b/i, field: "box" },
|
|
835
|
+
// One word only. Canada Post does publish two-word station names ("STN ST LAURENT"), but
|
|
836
|
+
// "STN MAIN BRANDON" is ambiguous between a two-word station and a station plus a community,
|
|
837
|
+
// and guessing wrong swallows the only part that can be geocoded. Missing the second word of
|
|
838
|
+
// a rare station name costs a label; stealing the town costs the answer.
|
|
839
|
+
{ pattern: /\b(?:STN|STATION|RPO|LCD)\s+([A-Z][A-Z0-9'-]*)\b/i, field: "station" }
|
|
840
|
+
];
|
|
841
|
+
function parseRuralRoute(q) {
|
|
842
|
+
if (typeof q !== "string") return null;
|
|
843
|
+
const raw = q.trim();
|
|
844
|
+
if (raw === "") return null;
|
|
845
|
+
const matched = KINDS.map((k) => ({ ...k, match: k.pattern.exec(raw) })).find((k) => k.match !== null);
|
|
846
|
+
if (!matched?.match) return null;
|
|
847
|
+
let rest = raw.replace(matched.pattern, " ");
|
|
848
|
+
const parsed = { raw, kind: matched.kind, remainder: "" };
|
|
849
|
+
if (matched.numbered) parsed.number = String(Number(matched.match[1]));
|
|
850
|
+
for (const part of PARTS) {
|
|
851
|
+
const hit = part.pattern.exec(rest);
|
|
852
|
+
if (!hit) continue;
|
|
853
|
+
parsed[part.field] = hit[1].trim().toUpperCase();
|
|
854
|
+
rest = rest.replace(part.pattern, " ");
|
|
855
|
+
}
|
|
856
|
+
parsed.remainder = rest.replace(/[,;]+/g, " ").replace(/\s+/g, " ").replace(/^[-\s]+|[-\s]+$/g, "");
|
|
857
|
+
return parsed;
|
|
858
|
+
}
|
|
859
|
+
function looksLikeRuralRoute(q) {
|
|
860
|
+
return parseRuralRoute(q) != null;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// src/places.ts
|
|
2
864
|
import { DEFAULT_GATEWAY } from "@unmap/core";
|
|
865
|
+
var PlacesError = class extends Error {
|
|
866
|
+
constructor(status, message, code) {
|
|
867
|
+
super(message);
|
|
868
|
+
this.status = status;
|
|
869
|
+
this.code = code;
|
|
870
|
+
this.name = "PlacesError";
|
|
871
|
+
}
|
|
872
|
+
status;
|
|
873
|
+
code;
|
|
874
|
+
};
|
|
875
|
+
async function errorFrom(res, path) {
|
|
876
|
+
const fallback = `places ${res.status} on ${path}`;
|
|
877
|
+
let body;
|
|
878
|
+
try {
|
|
879
|
+
body = await res.json();
|
|
880
|
+
} catch {
|
|
881
|
+
body = void 0;
|
|
882
|
+
}
|
|
883
|
+
const reason = typeof body?.error === "string" ? body.error : void 0;
|
|
884
|
+
const code = typeof body?.code === "string" ? body.code : void 0;
|
|
885
|
+
return new PlacesError(res.status, reason ? `${reason} (${fallback})` : fallback, code);
|
|
886
|
+
}
|
|
887
|
+
var Places = class {
|
|
888
|
+
key;
|
|
889
|
+
base;
|
|
890
|
+
fetchImpl;
|
|
891
|
+
constructor(opts) {
|
|
892
|
+
this.key = opts.key;
|
|
893
|
+
this.base = (opts.gateway ?? DEFAULT_GATEWAY).replace(/\/+$/, "");
|
|
894
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Search places by name.
|
|
898
|
+
*
|
|
899
|
+
* Unlike `Geocoder.search()`, which returns whatever best matches and for "Calgary" is the city
|
|
900
|
+
* itself, this returns places.
|
|
901
|
+
*/
|
|
902
|
+
search(q, opts = {}) {
|
|
903
|
+
return this.request("/places/search", {
|
|
904
|
+
q,
|
|
905
|
+
limit: opts.limit != null ? String(opts.limit) : void 0,
|
|
906
|
+
lang: opts.lang,
|
|
907
|
+
bbox: opts.bbox?.join(",")
|
|
908
|
+
}, opts.signal);
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Places of a category near a point.
|
|
912
|
+
*
|
|
913
|
+
* `category` takes a taxonomy id (`health.pharmacy`) or a word in English or French
|
|
914
|
+
* (`pharmacy`, `pharmacie`); the gateway resolves aliases, so `"boulangerie"` works.
|
|
915
|
+
*/
|
|
916
|
+
nearby(category, opts = {}) {
|
|
917
|
+
return this.request("/places/nearby", {
|
|
918
|
+
category: Array.isArray(category) ? category.join(",") : String(category),
|
|
919
|
+
near: opts.near?.join(","),
|
|
920
|
+
bbox: opts.bbox?.join(","),
|
|
921
|
+
radius: opts.radius != null ? String(opts.radius) : void 0,
|
|
922
|
+
limit: opts.limit != null ? String(opts.limit) : void 0,
|
|
923
|
+
lang: opts.lang
|
|
924
|
+
}, opts.signal);
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* One place by its unmap id.
|
|
928
|
+
*
|
|
929
|
+
* Throws `PlacesError` with status 404 when it does not exist. A malformed id is also a 404,
|
|
930
|
+
* because from a caller's side "does not exist" and "is not a well-formed id" are the same
|
|
931
|
+
* answer; the gateway's message distinguishes them as far as it honestly can.
|
|
932
|
+
*/
|
|
933
|
+
get(id, opts = {}) {
|
|
934
|
+
return this.request(`/places/${encodeURIComponent(id)}`, { lang: opts.lang }, opts.signal);
|
|
935
|
+
}
|
|
936
|
+
async request(path, params, signal) {
|
|
937
|
+
const q = new URLSearchParams({ key: this.key });
|
|
938
|
+
for (const [k, v] of Object.entries(params)) if (v != null && v !== "") q.set(k, v);
|
|
939
|
+
const res = await this.fetchImpl(`${this.base}${path}?${q}`, {
|
|
940
|
+
signal,
|
|
941
|
+
headers: { accept: "application/json" }
|
|
942
|
+
});
|
|
943
|
+
if (!res.ok) throw await errorFrom(res, path);
|
|
944
|
+
return await res.json();
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
// src/index.ts
|
|
3
949
|
var GeocoderError = class extends Error {
|
|
4
950
|
constructor(status, message, code) {
|
|
5
951
|
super(message);
|
|
@@ -10,7 +956,7 @@ var GeocoderError = class extends Error {
|
|
|
10
956
|
status;
|
|
11
957
|
code;
|
|
12
958
|
};
|
|
13
|
-
async function
|
|
959
|
+
async function errorFrom2(res, path) {
|
|
14
960
|
const fallback = `geocode ${res.status} on ${path}`;
|
|
15
961
|
let body;
|
|
16
962
|
try {
|
|
@@ -28,7 +974,7 @@ var Geocoder = class {
|
|
|
28
974
|
fetchImpl;
|
|
29
975
|
constructor(opts) {
|
|
30
976
|
this.key = opts.key;
|
|
31
|
-
this.base = (opts.gateway ??
|
|
977
|
+
this.base = (opts.gateway ?? DEFAULT_GATEWAY2).replace(/\/+$/, "");
|
|
32
978
|
this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
33
979
|
}
|
|
34
980
|
/** Full-text search. */
|
|
@@ -39,6 +985,32 @@ var Geocoder = class {
|
|
|
39
985
|
autocomplete(q, opts = {}) {
|
|
40
986
|
return this.query("/geocode/autocomplete", { q, ...toParams(opts) }, opts.signal);
|
|
41
987
|
}
|
|
988
|
+
/**
|
|
989
|
+
* Forward geocoding from address components rather than one string.
|
|
990
|
+
*
|
|
991
|
+
* `address` and `city` decide which rows are candidates and which wins. `region` and
|
|
992
|
+
* `postalcode` narrow: they drop rows that contradict them and keep rows that are silent
|
|
993
|
+
* about them, which come back with `region_not_verified` / `postalcode_not_verified` in
|
|
994
|
+
* `match_reasons`. Needs at least one of `address`, `city` or `postalcode`.
|
|
995
|
+
*/
|
|
996
|
+
structured(opts) {
|
|
997
|
+
return this.query(
|
|
998
|
+
"/geocode/structured",
|
|
999
|
+
{
|
|
1000
|
+
address: opts.address,
|
|
1001
|
+
city: opts.city,
|
|
1002
|
+
region: opts.region,
|
|
1003
|
+
postalcode: opts.postalcode,
|
|
1004
|
+
country: opts.country,
|
|
1005
|
+
limit: opts.limit != null ? String(opts.limit) : void 0,
|
|
1006
|
+
lang: opts.lang,
|
|
1007
|
+
bbox: opts.bbox ? opts.bbox.join(",") : void 0,
|
|
1008
|
+
focus: opts.focus ? opts.focus.join(",") : void 0,
|
|
1009
|
+
layers: opts.layers
|
|
1010
|
+
},
|
|
1011
|
+
opts.signal
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
42
1014
|
/**
|
|
43
1015
|
* Category search: POIs of a kind near a point, sorted by distance (metres, in each
|
|
44
1016
|
* result's `distance`). `category` takes canonical taxonomy ids ("health.pharmacy") or
|
|
@@ -67,15 +1039,54 @@ var Geocoder = class {
|
|
|
67
1039
|
opts.signal
|
|
68
1040
|
);
|
|
69
1041
|
}
|
|
70
|
-
|
|
1042
|
+
/**
|
|
1043
|
+
* Which catalog features contain a point. Default layer is `municipalities`
|
|
1044
|
+
* (core). `survey.dls` / `survey.nts` need the `legal-land` add-on.
|
|
1045
|
+
* `energy.*` need the `energy` add-on. `mining.*` need the `mining` add-on
|
|
1046
|
+
* and identify via Land extra tables. Empty until the matching PostGIS table
|
|
1047
|
+
* is loaded. No mining search grammar — claim numbers stay `energy.mineral-tenure`.
|
|
1048
|
+
*/
|
|
1049
|
+
identify(opts) {
|
|
1050
|
+
return this.requestJson(
|
|
1051
|
+
"/identify",
|
|
1052
|
+
{
|
|
1053
|
+
at: opts.at.join(","),
|
|
1054
|
+
layers: opts.layers?.length ? opts.layers.join(",") : void 0,
|
|
1055
|
+
geometry: opts.geometry ? "true" : void 0
|
|
1056
|
+
},
|
|
1057
|
+
opts.signal
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Features of one catalog layer that intersect a bbox. Same PostGIS tables as
|
|
1062
|
+
* `identify`. Tile-only catalog ids are a 501. Survey layers need `legal-land`;
|
|
1063
|
+
* `energy.*` need `energy`; `survey.parcels` needs `agriculture`; mining
|
|
1064
|
+
* layers need `mining`. Empty until the matching table is loaded.
|
|
1065
|
+
*/
|
|
1066
|
+
queryLayer(opts) {
|
|
1067
|
+
return this.requestJson(
|
|
1068
|
+
"/data/query",
|
|
1069
|
+
{
|
|
1070
|
+
layer: opts.layer,
|
|
1071
|
+
bbox: opts.bbox.join(","),
|
|
1072
|
+
limit: opts.limit != null ? String(opts.limit) : void 0,
|
|
1073
|
+
geometry: opts.geometry ? "true" : void 0
|
|
1074
|
+
},
|
|
1075
|
+
opts.signal
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
async requestJson(path, params, signal) {
|
|
71
1079
|
const q = new URLSearchParams({ key: this.key });
|
|
72
1080
|
for (const [k, v] of Object.entries(params)) if (v != null && v !== "") q.set(k, v);
|
|
73
1081
|
const res = await this.fetchImpl(`${this.base}${path}?${q}`, {
|
|
74
1082
|
signal,
|
|
75
1083
|
headers: { accept: "application/json" }
|
|
76
1084
|
});
|
|
77
|
-
if (!res.ok) throw await
|
|
78
|
-
|
|
1085
|
+
if (!res.ok) throw await errorFrom2(res, path);
|
|
1086
|
+
return res.json();
|
|
1087
|
+
}
|
|
1088
|
+
async query(path, params, signal) {
|
|
1089
|
+
const body = await this.requestJson(path, params, signal);
|
|
79
1090
|
return Array.isArray(body) ? body : body.results ?? [];
|
|
80
1091
|
}
|
|
81
1092
|
};
|
|
@@ -83,11 +1094,71 @@ function toParams(o) {
|
|
|
83
1094
|
return {
|
|
84
1095
|
limit: o.limit != null ? String(o.limit) : void 0,
|
|
85
1096
|
lang: o.lang,
|
|
86
|
-
bbox: o.bbox ? o.bbox.join(",") : void 0
|
|
1097
|
+
bbox: o.bbox ? o.bbox.join(",") : void 0,
|
|
1098
|
+
focus: o.focus ? o.focus.join(",") : void 0,
|
|
1099
|
+
region: o.region,
|
|
1100
|
+
geometry: o.geometry ? "true" : void 0,
|
|
1101
|
+
layers: o.layers
|
|
87
1102
|
};
|
|
88
1103
|
}
|
|
89
1104
|
export {
|
|
1105
|
+
AMBIGUOUS_SCORE_RATIO,
|
|
1106
|
+
CIVIC_SEARCH_LAYERS,
|
|
1107
|
+
CsvParser,
|
|
1108
|
+
DEFAULT_IDENTIFY_LAYERS,
|
|
1109
|
+
DEFAULT_QUERY_LIMIT,
|
|
1110
|
+
ENERGY_IDENTIFY_LAYER_IDS,
|
|
1111
|
+
ENERGY_SEARCH_LAYERS,
|
|
90
1112
|
Geocoder,
|
|
91
|
-
GeocoderError
|
|
1113
|
+
GeocoderError,
|
|
1114
|
+
IDENTIFY_LAYER_IDS,
|
|
1115
|
+
LAND_IDENTIFY_LAYER_IDS,
|
|
1116
|
+
MAX_QUERY_LIMIT,
|
|
1117
|
+
MINING_IDENTIFY_LAYER_IDS,
|
|
1118
|
+
OUTPUT_COLUMNS,
|
|
1119
|
+
OUTPUT_PREFIX,
|
|
1120
|
+
Places,
|
|
1121
|
+
PlacesError,
|
|
1122
|
+
QUERY_LAYER_IDS,
|
|
1123
|
+
REGIONS,
|
|
1124
|
+
REGION_CODES,
|
|
1125
|
+
REGION_KEYS,
|
|
1126
|
+
STRUCTURED_FIELDS,
|
|
1127
|
+
alignRow,
|
|
1128
|
+
classify,
|
|
1129
|
+
encodeField,
|
|
1130
|
+
encodeRow,
|
|
1131
|
+
energySearchRequested,
|
|
1132
|
+
escapeForSpreadsheet,
|
|
1133
|
+
isEnergyIdentifyLayer,
|
|
1134
|
+
isEnergySearchSource,
|
|
1135
|
+
isIdentifyLayer,
|
|
1136
|
+
isLandIdentifyLayer,
|
|
1137
|
+
knownIdentifyLayers,
|
|
1138
|
+
knownQueryLayers,
|
|
1139
|
+
knownSearchLayers,
|
|
1140
|
+
looksLikeEnergyQuery,
|
|
1141
|
+
looksLikeFormula,
|
|
1142
|
+
looksLikeLegalLand,
|
|
1143
|
+
looksLikeParcel,
|
|
1144
|
+
looksLikeRuralRoute,
|
|
1145
|
+
outputValues,
|
|
1146
|
+
parseEnergyQuery,
|
|
1147
|
+
parseEnergySearchLayers,
|
|
1148
|
+
parseFocus,
|
|
1149
|
+
parseIdentifyAt,
|
|
1150
|
+
parseIdentifyLayers,
|
|
1151
|
+
parseLegalLand,
|
|
1152
|
+
parseParcel,
|
|
1153
|
+
parseQueryBbox,
|
|
1154
|
+
parseQueryLayer,
|
|
1155
|
+
parseQueryLimit,
|
|
1156
|
+
parseRegion,
|
|
1157
|
+
parseRuralRoute,
|
|
1158
|
+
parseSearchLayers,
|
|
1159
|
+
parseStructuredQuery,
|
|
1160
|
+
regionForms,
|
|
1161
|
+
resolveOutputHeader,
|
|
1162
|
+
toLegalLandDetails
|
|
92
1163
|
};
|
|
93
1164
|
//# sourceMappingURL=index.js.map
|