@unmap/geocoding 0.9.1 → 0.12.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
@@ -1,10 +1,614 @@
1
+ /**
2
+ * Framework-free port of Township Canada's legal-land regex grammar
3
+ * (`api/packages/shared/regex_rules.js` + `helpers.processInput`).
4
+ *
5
+ * A successful parse is a structured candidate, not a geocoder hit. Coordinates,
6
+ * civic addresses, and place names return null — those stay on the civic path.
7
+ */
8
+ type LegalLandSystem = "DLS" | "NTS" | "GTS" | "FPS" | "RIVER_LOT";
9
+ type LegalLandUnit = "lsd" | "qtr" | "half" | "sec" | "twp" | "nts" | "nts_sheet" | "lot" | "con" | "grid" | "section" | "unit" | "river_lot";
10
+ type LegalLandComponents = {
11
+ meridian?: string;
12
+ range?: string;
13
+ township?: string;
14
+ section?: string;
15
+ quarter?: string;
16
+ half?: string;
17
+ lsd?: string;
18
+ quarterUnit?: string;
19
+ ntsUnit?: string;
20
+ block?: string;
21
+ series?: string;
22
+ mapArea?: string;
23
+ mapSheet?: string;
24
+ fpsUnit?: string;
25
+ fpsSection?: string;
26
+ latDeg?: string;
27
+ latMin?: string;
28
+ lonDeg?: string;
29
+ lonMin?: string;
30
+ lot?: string;
31
+ concession?: string;
32
+ townshipName?: string;
33
+ kind?: string;
34
+ parts?: string[];
35
+ };
36
+ type LegalLandParse = {
37
+ system: LegalLandSystem;
38
+ canonical: string;
39
+ unit: LegalLandUnit;
40
+ components: LegalLandComponents;
41
+ province?: string;
42
+ };
43
+ declare function parseLegalLand(q: string): LegalLandParse | null;
44
+ /** Public `legal_land.system` values on a geocode result (LOCATION-MODEL). */
45
+ type LegalLandLayerSystem = "dls" | "nts" | "fps" | "on" | "river_lot";
46
+ /** True when `q` is a well-formed legal-land description, not a civic query. */
47
+ declare function looksLikeLegalLand(q: string): boolean;
48
+ interface LegalLandDetails {
49
+ system: LegalLandLayerSystem;
50
+ province?: string;
51
+ canonical: string;
52
+ unit: LegalLandUnit;
53
+ components: LegalLandComponents;
54
+ }
55
+ /** Map a parse onto the public result block. Does not invent coordinates. */
56
+ declare function toLegalLandDetails(parsed: LegalLandParse): LegalLandDetails;
57
+
58
+ /**
59
+ * Point-in-polygon identify: which catalog features contain a coordinate.
60
+ *
61
+ * Identify-capable layers are a subset of the gateway catalog. `parks`,
62
+ * `rail`, `aboriginal-lands`, `energy.ccs`, and `energy.geothermal` are
63
+ * catalogued for tiles/overlays but have no identify table, so they 400 here
64
+ * rather than return a silent miss. Energy tables are empty-safe: a missing
65
+ * relation is `[]` for that layer, not a 500.
66
+ */
67
+ declare const LAND_IDENTIFY_LAYER_IDS: readonly ["municipalities", "survey.dls", "survey.nts", "survey.parcels"];
68
+ declare const ENERGY_IDENTIFY_LAYER_IDS: readonly ["energy.wells", "energy.pipelines", "energy.facilities"];
69
+ /** Routed through the Land container as extra tables. Not a fourth container. */
70
+ declare const MINING_IDENTIFY_LAYER_IDS: readonly ["mining.occurrences", "mining.prospectivity", "mining.sk-potential"];
71
+ declare const IDENTIFY_LAYER_IDS: readonly ["municipalities", "survey.dls", "survey.nts", "survey.parcels", "energy.wells", "energy.pipelines", "energy.facilities", "energy.fields", "energy.petroleum-tenure", "energy.mineral-tenure", "mining.occurrences", "mining.prospectivity", "mining.sk-potential"];
72
+ type IdentifyLayerId = (typeof IDENTIFY_LAYER_IDS)[number];
73
+ /** Default when `layers` is omitted: core only, so an account key without add-ons can identify. */
74
+ declare const DEFAULT_IDENTIFY_LAYERS: readonly IdentifyLayerId[];
75
+ declare function isIdentifyLayer(id: string): id is IdentifyLayerId;
76
+ declare function isLandIdentifyLayer(id: IdentifyLayerId): boolean;
77
+ declare function isEnergyIdentifyLayer(id: IdentifyLayerId): boolean;
78
+ declare function knownIdentifyLayers(): string;
79
+ declare function parseIdentifyLayers(raw: string | undefined): {
80
+ layers: IdentifyLayerId[];
81
+ } | {
82
+ error: string;
83
+ };
84
+ declare function parseIdentifyAt(at: string | undefined, lon: string | undefined, lat: string | undefined): {
85
+ at: [number, number];
86
+ } | {
87
+ error: string;
88
+ };
89
+ interface IdentifyOptions {
90
+ /** `[lng, lat]` in WGS84 degrees. */
91
+ at: [number, number];
92
+ /** Identify-capable catalog ids. Default `municipalities`. Survey layers need `legal-land`. Energy layers need `energy`. `survey.parcels` needs `agriculture`. Mining layers need `mining`. */
93
+ layers?: readonly string[];
94
+ /** Include polygons when the row has one. */
95
+ geometry?: boolean;
96
+ signal?: AbortSignal;
97
+ }
98
+ interface IdentifyResult {
99
+ layer: IdentifyLayerId;
100
+ id: string;
101
+ name: string;
102
+ province?: string;
103
+ unit?: string;
104
+ survey_system?: string;
105
+ uwi?: string;
106
+ licence?: string;
107
+ operator?: string;
108
+ status?: string;
109
+ pid?: string;
110
+ geometry?: {
111
+ type: "Polygon" | "MultiPolygon";
112
+ coordinates: number[][][] | number[][][][];
113
+ };
114
+ }
115
+ interface IdentifyResponse {
116
+ at: [number, number];
117
+ results: IdentifyResult[];
118
+ }
119
+
120
+ /**
121
+ * Bbox query against identify-capable catalog layers. Same PostGIS tables as
122
+ * `/identify`; tile-only catalog ids are rejected (gateway 501), not silently empty.
123
+ */
124
+
125
+ declare const QUERY_LAYER_IDS: readonly ["municipalities", "survey.dls", "survey.nts", "survey.parcels", "energy.wells", "energy.pipelines", "energy.facilities", "energy.fields", "energy.petroleum-tenure", "energy.mineral-tenure", "mining.occurrences", "mining.prospectivity", "mining.sk-potential"];
126
+ type QueryLayerId = IdentifyLayerId;
127
+ declare const DEFAULT_QUERY_LIMIT = 50;
128
+ declare const MAX_QUERY_LIMIT = 50;
129
+ type QueryBbox = [number, number, number, number];
130
+ declare function knownQueryLayers(): string;
131
+ declare function parseQueryLayer(raw: string | undefined): {
132
+ layer: QueryLayerId;
133
+ } | {
134
+ unknown: string;
135
+ error: string;
136
+ } | {
137
+ error: string;
138
+ };
139
+ declare function parseQueryBbox(raw: string | undefined): {
140
+ bbox: QueryBbox;
141
+ } | {
142
+ error: string;
143
+ };
144
+ /** Missing or non-numeric `limit` falls back; a finite value is clamped to 1–50. */
145
+ declare function parseQueryLimit(raw: string | undefined): number;
146
+ interface QueryOptions {
147
+ /** Identify-capable catalog id. Tile-only catalog ids are a 501. */
148
+ layer: string;
149
+ /** `[minLng, minLat, maxLng, maxLat]` in WGS84 degrees. */
150
+ bbox: QueryBbox;
151
+ /** Max features. Default 50, clamped to 1–50. */
152
+ limit?: number;
153
+ /** Include geometry when the row has one. */
154
+ geometry?: boolean;
155
+ signal?: AbortSignal;
156
+ }
157
+ interface QueryResponse {
158
+ layer: QueryLayerId;
159
+ bbox: QueryBbox;
160
+ results: IdentifyResult[];
161
+ }
162
+
163
+ /**
164
+ * Structured forward geocoding: the caller hands over the components of an address instead of
165
+ * one string, and the engine keeps them apart all the way through.
166
+ *
167
+ * The distinction that matters, and the one the docs state plainly: some fields CONSTRAIN the
168
+ * result set and some only RANK it.
169
+ *
170
+ * - `address` and `city` are retrieval and ranking. They decide which rows are candidates and
171
+ * which candidate wins.
172
+ * - `region` and `postalcode` constrain, under one rule: **a supplied component excludes
173
+ * records that contradict it, and never excludes records that are silent about it.** A row
174
+ * in Ontario is out when you asked for Alberta; a row with no province at all stays in and
175
+ * comes back with `region_not_verified` in its `match_reasons`.
176
+ * - `country` is validated, not matched: the corpus is Canada only.
177
+ *
178
+ * The alternative — joining the fields into one string and running the free-text engine — is
179
+ * what this exists instead of. It would make `region` a scoring token that any row could match
180
+ * by coincidence, which is how "Kelowna, BC" used to return the province of British Columbia.
181
+ */
182
+ interface StructuredQuery {
183
+ /** Street line: civic number and street name, as the caller would write it on an envelope. */
184
+ address?: string;
185
+ city?: string;
186
+ /** Two-letter code, normalised from any accepted spelling ("Québec" and "qc" both give "QC"). */
187
+ region?: string;
188
+ /** Six characters, no space, uppercase ("M5H2N2"). */
189
+ postalcode?: string;
190
+ /** Always "CA". Present so the field can be supplied and validated, not so it can vary. */
191
+ country?: string;
192
+ }
193
+ interface StructuredOptions extends StructuredQuery {
194
+ limit?: number;
195
+ lang?: string;
196
+ /** [minLng, minLat, maxLng, maxLat] hard filter, same semantics as `search`. */
197
+ bbox?: [number, number, number, number];
198
+ /** [lng, lat] soft proximity preference. Moves ranking toward the point, excludes nothing. */
199
+ focus?: [number, number];
200
+ /** Civic result types to allow (`address`, `street`, `locality`, `region`, `poi`). */
201
+ layers?: string;
202
+ signal?: AbortSignal;
203
+ }
204
+ /** The query fields, in the order the docs and the CLI list them. */
205
+ declare const STRUCTURED_FIELDS: readonly ["address", "city", "region", "postalcode", "country"];
206
+ type StructuredParseResult = {
207
+ query: StructuredQuery;
208
+ } | {
209
+ error: string;
210
+ };
211
+ /** Normalise and validate raw structured input. Returns the cleaned query or one error string. */
212
+ declare function parseStructuredQuery(raw: Partial<Record<string, string | undefined>>): StructuredParseResult;
213
+
214
+ /**
215
+ * Energy-query detect for wells / facilities.
216
+ *
217
+ * UWI grammars are Township Canada's `web/server/utils/regex.js` (`uwi`, `bc_uwi`).
218
+ * Well licences are only treated as energy when they carry a clear prefix (`W`,
219
+ * `WL`, `LIC` / `LICENCE` / `LICENSE`). Bare digits stay civic.
220
+ * Facility names are too generic: they are energy only when the caller asks
221
+ * (`?layers=well|facility` or `profile`/`source=energy`).
222
+ */
223
+ declare const ENERGY_SEARCH_LAYERS: readonly ["well", "facility"];
224
+ type EnergySearchLayer = (typeof ENERGY_SEARCH_LAYERS)[number];
225
+ type EnergyQueryKind = "uwi" | "licence";
226
+ interface EnergyQuery {
227
+ kind: EnergyQueryKind;
228
+ /** Trimmed original. */
229
+ raw: string;
230
+ /** Lookup key: UWI uppercased, or licence digits. */
231
+ key: string;
232
+ }
233
+ declare function parseEnergyQuery(q: string): EnergyQuery | null;
234
+ /** True when `q` is a UWI or a prefixed well licence, not a civic address. */
235
+ declare function looksLikeEnergyQuery(q: string): boolean;
236
+ declare function parseEnergySearchLayers(raw: string | undefined): EnergySearchLayer[];
237
+ declare function isEnergySearchSource(raw: string | undefined): boolean;
238
+ /**
239
+ * Whether this search should take the energy branch (addon gate + table lookup).
240
+ * Civic strings stay civic unless the caller asked for energy layers/source.
241
+ */
242
+ declare function energySearchRequested(q: string, opts?: {
243
+ layers?: string;
244
+ profile?: string;
245
+ source?: string;
246
+ }): boolean;
247
+ interface EnergyDetails {
248
+ uwi?: string;
249
+ licence?: string;
250
+ operator?: string;
251
+ status?: string;
252
+ }
253
+
254
+ /**
255
+ * RFC 4180 CSV, streaming, with the Canadian-address-file details that break naive splitters.
256
+ *
257
+ * Hand-written rather than a dependency for two reasons that both matter here. `@unmap/cli` is
258
+ * deliberately a zero-runtime-dependency Node tool, and the hosted batch consumer is a
259
+ * Cloudflare Worker — one codec that runs in both, with no bundler shims, is worth the ~150
260
+ * lines. The other reason is that the failure modes below are the whole job: a parser that gets
261
+ * them wrong corrupts a customer's file silently, which is worse than refusing it.
262
+ *
263
+ * What it handles, and why each one is here:
264
+ *
265
+ * - **A UTF-8 BOM.** Excel writes one. Left in place it becomes part of the first header name,
266
+ * so `--query-column address` fails to find `address` and the error blames the user.
267
+ * - **Quoted commas and quoted newlines.** "Gatineau, QC" and a multi-line note field are
268
+ * ordinary in address exports. A record can therefore span several lines, which is why a
269
+ * hosted job cannot chunk a CSV at arbitrary newlines.
270
+ * - **Escaped quotes** (`""` inside a quoted field).
271
+ * - **CRLF, LF and a lone CR** as record separators, mixed within one file.
272
+ * - **Leading zeros** — postal codes and civic numbers. Everything stays a string, always.
273
+ * - **Ragged rows.** A row with fewer or more fields than the header is a fact about the file,
274
+ * not a crash: it is reported as an error for that row and the rest of the file continues.
275
+ *
276
+ * The parser is a state machine over chunks, so a 50 MiB file never has to be a 50 MiB string.
277
+ */
278
+ interface CsvRecord {
279
+ /** 1-based index among data records, excluding the header. Stable across resumes. */
280
+ index: number;
281
+ /** Field values in header order, exactly as they appeared. Never coerced. */
282
+ values: string[];
283
+ }
284
+ interface CsvProblem {
285
+ index: number;
286
+ code: "too_few_fields" | "too_many_fields";
287
+ message: string;
288
+ }
289
+ /**
290
+ * Incremental parser. Feed it strings; it yields complete records.
291
+ *
292
+ * Deliberately not an async iterator: the caller owns the read loop, which is what lets the CLI
293
+ * and the Worker use their own very different stream APIs over the same state machine.
294
+ */
295
+ declare class CsvParser {
296
+ private field;
297
+ private row;
298
+ private inQuotes;
299
+ /** A quote seen inside a quoted field: either an escape or the close. */
300
+ private pendingQuote;
301
+ /** A CR seen outside quotes: end of record, and a following LF is part of the same break. */
302
+ private pendingCr;
303
+ private started;
304
+ private rowHasContent;
305
+ /**
306
+ * Feed one chunk. Returns the records completed by it.
307
+ *
308
+ * A record that ends exactly at a chunk boundary is returned here; a partial one is held until
309
+ * the next chunk, or until `end()`.
310
+ */
311
+ write(chunk: string): string[][];
312
+ /** Flush a final record with no trailing newline. */
313
+ end(): string[][];
314
+ /** True when the stream ended inside an unterminated quoted field. */
315
+ get unterminated(): boolean;
316
+ private finishRow;
317
+ }
318
+ /**
319
+ * Align a raw row against the header.
320
+ *
321
+ * Ragged rows are reported, not repaired and not thrown: a short row is padded so the caller can
322
+ * still carry its original values into the output, and a long row keeps its extra fields for the
323
+ * same reason. Discarding either would lose customer data in a file we were only asked to
324
+ * annotate.
325
+ */
326
+ declare function alignRow(header: readonly string[], values: readonly string[], index: number): {
327
+ record: CsvRecord;
328
+ problem?: CsvProblem;
329
+ };
330
+ /**
331
+ * Quote a value for output: only when it has to be, and always the same way.
332
+ *
333
+ * The tab is in the set even though RFC 4180 does not require it. It is there for
334
+ * `escapeForSpreadsheet` below, which neutralises a formula by prefixing one: unquoted, a
335
+ * leading tab is read inconsistently across spreadsheet apps, and quoted it is unambiguous
336
+ * everywhere. A tab anywhere else in a field is rare enough that quoting it costs nothing.
337
+ */
338
+ declare function encodeField(value: string): string;
339
+ /**
340
+ * Whether a value would be read as a formula by a spreadsheet.
341
+ *
342
+ * Excel, Sheets and LibreOffice all evaluate a cell beginning `=`, `+`, `-`, `@`, and CSV is the
343
+ * classic delivery vehicle for that (CSV injection). It matters here because a geocoder's output
344
+ * is a file people open in a spreadsheet, and because Canadian address data really does contain
345
+ * values that start this way — a negative latitude, "-123 Main St" in a malformed row.
346
+ *
347
+ * Detection is separated from mitigation on purpose: `escapeForSpreadsheet` prefixes a tab so
348
+ * the cell displays as text, and the caller decides whether to apply it, because prefixing
349
+ * changes the bytes and a pipeline reading the file programmatically wants the original.
350
+ */
351
+ declare function looksLikeFormula(value: string): boolean;
352
+ declare function escapeForSpreadsheet(value: string): string;
353
+ /** One output line, terminated with CRLF (what RFC 4180 specifies and Excel expects). */
354
+ declare function encodeRow(values: readonly string[], opts?: {
355
+ spreadsheetSafe?: boolean;
356
+ }): string;
357
+
358
+ /**
359
+ * The output schema a batch geocode writes, shared by the local CLI and the hosted service.
360
+ *
361
+ * One schema, one place, because the two are meant to be interchangeable: a customer who
362
+ * prototypes with `unmap geocode csv` and then moves the job to the dashboard should get the
363
+ * same columns in the same order with the same meanings, and diffing the two files should show
364
+ * nothing but the coordinates.
365
+ */
366
+ /** Prefix on every column this adds, so a customer's own `status` column is never shadowed. */
367
+ declare const OUTPUT_PREFIX = "unmap_";
368
+ /**
369
+ * What happened to one input row. Distinct from `match_type`, which is about the QUALITY of an
370
+ * answer — a row can have `status: "matched"` and `match_type: "fallback"`, and that pairing is
371
+ * the common case worth reviewing.
372
+ */
373
+ type RowStatus =
374
+ /** A result came back and is good enough to use without looking at it. */
375
+ "matched"
376
+ /** A result came back, but something about it should be looked at. See `unmap_review`. */
377
+ | "review"
378
+ /** The query ran and the corpus has nothing. Not an error, and it is billed. */
379
+ | "no_match"
380
+ /** The row never became a query: no address to send. Not billed. */
381
+ | "invalid"
382
+ /** The query ran and failed permanently (bad credentials, exhausted budget, repeated 5xx). */
383
+ | "failed"
384
+ /** Not attempted: the run was cancelled, paused or interrupted before reaching this row. */
385
+ | "unprocessed";
386
+ /**
387
+ * Why a row is in the review file. Row-level, and deliberately a superset of the result-level
388
+ * `match_reasons`: two of these are about the SHAPE of the answer rather than its content.
389
+ */
390
+ type ReviewReason = GeocodeMatchReason
391
+ /** The result is coarser than the row asked for. */
392
+ | "fallback_match"
393
+ /** More than one candidate scored closely; the top one may not be the intended one. */
394
+ | "ambiguous"
395
+ /** Nothing came back at all. */
396
+ | "no_match"
397
+ /** The row could not be turned into a query. */
398
+ | "invalid_input";
399
+ declare const OUTPUT_COLUMNS: readonly ["unmap_status", "unmap_id", "unmap_name", "unmap_lng", "unmap_lat", "unmap_layer", "unmap_source", "unmap_match_type", "unmap_precision", "unmap_review"];
400
+ type OutputColumn = (typeof OUTPUT_COLUMNS)[number];
401
+ /**
402
+ * Resolve a collision between the columns this writes and the customer's own header.
403
+ *
404
+ * Silently overwriting a column called `unmap_status` would destroy data; refusing the file
405
+ * outright would strand anyone who ran the tool twice on its own output, which is exactly what
406
+ * people do. So a collision is suffixed (`unmap_status_2`) and reported, and the caller decides
407
+ * whether to say something. Comparison is case-insensitive because spreadsheets are.
408
+ */
409
+ declare function resolveOutputHeader(inputHeader: readonly string[]): {
410
+ header: string[];
411
+ renamed: Array<{
412
+ column: OutputColumn;
413
+ to: string;
414
+ }>;
415
+ };
416
+ /**
417
+ * When the top result is close enough to the runner-up that picking it is a coin flip.
418
+ *
419
+ * Ratio, not a difference: BM25 scores are not on an absolute scale and are not comparable
420
+ * across queries, so "within 5%" means something for every query while "within 2 points" means
421
+ * something only for the query it was tuned on. 0.95 is deliberately tight — this flags a row
422
+ * for a human to look at, and a review file that flags a third of the rows gets ignored whole.
423
+ */
424
+ declare const AMBIGUOUS_SCORE_RATIO = 0.95;
425
+ /**
426
+ * Classify one row's outcome from what the API returned.
427
+ *
428
+ * The rule the whole review file rests on: **nothing is silently discarded.** Every input row
429
+ * gets exactly one record in the main output, and every row that is not plainly usable gets a
430
+ * reason here and a line in the review file. A row that "failed quietly" is the outcome this
431
+ * function exists to make impossible.
432
+ */
433
+ declare function classify(results: readonly GeocodeResult[]): {
434
+ status: RowStatus;
435
+ reasons: ReviewReason[];
436
+ };
437
+ /** The ten values, in `OUTPUT_COLUMNS` order, for one row. */
438
+ declare function outputValues(status: RowStatus, reasons: readonly ReviewReason[], top: GeocodeResult | undefined): string[];
439
+
440
+ /**
441
+ * `layers=` on `/geocode/search`, `/geocode/autocomplete` and `/geocode/structured`.
442
+ *
443
+ * The parameter already existed and already meant one thing: `layers=well,facility` sends the
444
+ * query to the energy container instead of the civic one. Civic result types are added to the
445
+ * same parameter rather than to a new one, because from the caller's side they are the same
446
+ * question — "which kinds of thing may come back" — and two parameters answering it would have
447
+ * to define what happens when both are set.
448
+ *
449
+ * What the two sets do is genuinely different, and that is why they cannot be mixed:
450
+ * `well`/`facility` select a BACKEND (a different container, a different corpus, gated by the
451
+ * `energy` add-on); the civic values FILTER results inside one corpus. `layers=address,well`
452
+ * has no meaning that is not a guess, so it is a 400 rather than a silent choice.
453
+ *
454
+ * Unknown values are also a 400. That is a tightening: before this, unrecognised tokens were
455
+ * dropped silently, so `layers=adress` searched everything and looked like it had worked.
456
+ */
457
+ declare const CIVIC_SEARCH_LAYERS: readonly ["address", "street", "locality", "region", "poi"];
458
+ type CivicSearchLayer = (typeof CIVIC_SEARCH_LAYERS)[number];
459
+ type SearchLayersParse = {
460
+ kind: "none";
461
+ } | {
462
+ kind: "civic";
463
+ layers: CivicSearchLayer[];
464
+ } | {
465
+ kind: "energy";
466
+ layers: EnergySearchLayer[];
467
+ } | {
468
+ error: string;
469
+ };
470
+ /** Every accepted value, for error messages and docs. */
471
+ declare function knownSearchLayers(): string;
472
+ declare function parseSearchLayers(raw: string | undefined): SearchLayersParse;
473
+ /**
474
+ * `focus=lng,lat`: a soft proximity preference.
475
+ *
476
+ * It moves ranking toward the point and excludes nothing — a relevant result on the other side
477
+ * of the country still comes back, behind the near ones. That is the whole distinction from
478
+ * `bbox`, which is a hard filter, and the docs and the UI copy have to keep saying it: "prefer
479
+ * nearby" is not "restrict to this area".
480
+ */
481
+ type FocusParse = {
482
+ focus: [number, number];
483
+ } | {
484
+ error: string;
485
+ } | undefined;
486
+ declare function parseFocus(raw: string | undefined): FocusParse;
487
+
488
+ /** Every recognised spelling, lowercase, mapped to its two-letter code. */
489
+ declare const REGIONS: Readonly<Record<string, string>>;
490
+ /**
491
+ * Spellings sorted longest first, so "newfoundland and labrador" is tried before
492
+ * "newfoundland". Computed after the dotted-form loop, so generated keys sort with the rest.
493
+ */
494
+ declare const REGION_KEYS: readonly string[];
495
+ /** The thirteen codes, for validation messages. */
496
+ declare const REGION_CODES: readonly string[];
497
+ /**
498
+ * Resolve any accepted spelling to its two-letter code. Accent- and case-insensitive, so
499
+ * "Québec", "quebec" and "QC" all give "QC". Undefined when nothing matches.
500
+ */
501
+ declare function parseRegion(raw: string | undefined | null): string | undefined;
502
+ /**
503
+ * Every spelling the corpus may hold for a region code, uppercased: the code itself plus each
504
+ * name that maps to it. The loaders do not normalise `region` (OpenAddresses passes the source
505
+ * value through, so `ONTARIO` and `ON` both occur), so a filter or boost must match all of them.
506
+ */
507
+ declare function regionForms(code: string): string[];
508
+
509
+ /**
510
+ * ParcelMap BC PID detect. Grammar matches Township Canada
511
+ * `web/server/api/parcels/search.get.js`: nine digits, optional dashes
512
+ * (`010-867-813` or `010867813`). PIN, plan number, and parcel-name
513
+ * fuzzy match are not distinctive enough to steal civic search.
514
+ */
515
+ interface ParcelQuery {
516
+ /** Trimmed original. */
517
+ raw: string;
518
+ /** Lookup key: `XXX-XXX-XXX`. */
519
+ pid: string;
520
+ }
521
+ declare function parseParcel(q: string): ParcelQuery | null;
522
+ /** True when `q` is a ParcelMap BC PID, not a civic address. */
523
+ declare function looksLikeParcel(q: string): boolean;
524
+
525
+ /**
526
+ * Canadian rural and alternate delivery addresses.
527
+ *
528
+ * A rural route is not a place. `RR 2` names a mail carrier's circuit through a community, and
529
+ * the same number exists in hundreds of communities; the site of the mailbox is not in any open
530
+ * dataset. So this does not geocode the route. It splits the Canada Post delivery terms off the
531
+ * front of a query, hands the rest to civic search, and returns the parsed terms alongside the
532
+ * community that was found. "RR 2 Okotoks" resolves to Okotoks, labelled rural route 2, which is
533
+ * the honest answer: the town is known, the mailbox is not.
534
+ *
535
+ * Recognised, per Canada Post's addressing guide:
536
+ * RR / R.R. / RURAL ROUTE a rural route, numbered
537
+ * SS / SUBURBAN SERVICE a suburban service route, numbered
538
+ * MR / MOBILE ROUTE a mobile route, numbered
539
+ * GD / GENERAL DELIVERY counter pickup, never numbered
540
+ * SITE / COMP(ARTMENT) / BOX the position within the route
541
+ * STN / RPO / LCD the retail outlet the route runs from
542
+ *
543
+ * Deliberately not a locality detector: a query has to carry one of the delivery keywords above
544
+ * for this to fire, so an ordinary civic address is untouched.
545
+ */
546
+ type RuralRouteKind = "rural-route" | "suburban-service" | "mobile-route" | "general-delivery";
547
+ interface RuralRouteQuery {
548
+ /** Trimmed original. */
549
+ raw: string;
550
+ kind: RuralRouteKind;
551
+ /** Route number as written. Absent for general delivery, which is never numbered. */
552
+ number?: string;
553
+ /** Position within the route, when given. */
554
+ site?: string;
555
+ compartment?: string;
556
+ box?: string;
557
+ /** The retail outlet the route runs from, e.g. "MAIN" in "STN MAIN". */
558
+ station?: string;
559
+ /**
560
+ * What is left after the delivery terms are removed: normally the community, and the only
561
+ * part that can actually be geocoded. Empty when the query was delivery terms and nothing
562
+ * else, which cannot be resolved to anywhere.
563
+ */
564
+ remainder: string;
565
+ }
566
+ declare function parseRuralRoute(q: string): RuralRouteQuery | null;
567
+ /**
568
+ * True when `q` carries Canada Post delivery terms.
569
+ *
570
+ * Note this is NOT a routing signal in the way `looksLikeLegalLand` is: a rural-route query is
571
+ * still answered by civic search, on its remainder. It exists so the caller can strip the terms
572
+ * and label the result, not so it can pick a different backend.
573
+ */
574
+ declare function looksLikeRuralRoute(q: string): boolean;
575
+
1
576
  interface GeocoderOptions {
2
577
  key: string;
3
578
  gateway?: string;
4
579
  /** Override the fetch implementation (tests, non-global environments). */
5
580
  fetch?: typeof fetch;
6
581
  }
7
- type GeocodeLayer = "address" | "street" | "locality" | "region" | "poi";
582
+ type GeocodeLayer = "address" | "street" | "locality" | "region" | "poi" | "legal_land" | "well" | "facility" | "parcel";
583
+ /**
584
+ * Which dataset the returned record came from. Conflation
585
+ * (`pipeline/geocoding/conflate.sql`) keeps one survivor per place and deletes the rest, so
586
+ * this is the SURVIVING record's source, not every dataset that agreed on the place. See
587
+ * `docs/services/geocoding-sources.md`.
588
+ */
589
+ type GeocodeSource = "osm" | "openaddresses" | "statcan_nar" | "nrcan_cgndb" | "wof" | "geonames";
590
+ /**
591
+ * How well the result answers what was asked, as a category — never a probability, and never
592
+ * derived from `score` (which stays raw BM25 relevance).
593
+ *
594
+ * - `exact` — every component the query supplied and the corpus supports was matched.
595
+ * - `partial` — something supplied did not match, or could not be verified.
596
+ * - `fallback` — the result is COARSER than the query asked for: a civic number was requested
597
+ * and the result carries none. A street or locality standing in for an address is always
598
+ * this, never `exact`.
599
+ * - `unknown` — input-match diagnostics do not apply (reverse, nearby, autocomplete).
600
+ */
601
+ type GeocodeMatchType = "exact" | "partial" | "fallback" | "unknown";
602
+ /**
603
+ * Positional resolution the SOURCE supports, separate from how well the input matched.
604
+ *
605
+ * `point` means a discrete point record in the source dataset — an address point or a POI. It
606
+ * is **not** a rooftop-accuracy claim, and it never means interpolated: the engine performs no
607
+ * interpolation. `locality` and `region` are centroids.
608
+ */
609
+ type GeocodePrecision = "point" | "street" | "locality" | "region" | "unknown";
610
+ /** Stable reason codes explaining a non-`exact` `match_type`. Additive; never renamed. */
611
+ type GeocodeMatchReason = "housenumber_not_matched" | "housenumber_differs" | "unit_not_verified" | "unit_differs" | "postalcode_not_verified" | "postalcode_differs" | "region_not_verified" | "region_differs" | "terms_unmatched" | "fuzzy_match";
8
612
  interface GeocodeResult {
9
613
  id: string;
10
614
  name: string;
@@ -12,6 +616,16 @@ interface GeocodeResult {
12
616
  lng: number;
13
617
  lat: number;
14
618
  score?: number;
619
+ /** Originating dataset of the returned record. */
620
+ source?: GeocodeSource | string;
621
+ /** That dataset's own identifier for the record, when it published one. */
622
+ source_id?: string;
623
+ /** How well the result answers the query. `unknown` where input matching does not apply. */
624
+ match_type?: GeocodeMatchType;
625
+ /** Positional resolution the source supports. Not an accuracy figure. */
626
+ precision?: GeocodePrecision;
627
+ /** Why `match_type` is not `exact`. Empty or absent when it is. */
628
+ match_reasons?: GeocodeMatchReason[];
15
629
  /** Canonical category id (e.g. "health.pharmacy") when the feature carries one. */
16
630
  category?: string;
17
631
  /** Spheroidal metres from the query anchor; only `/geocode/nearby` results carry it. */
@@ -24,13 +638,43 @@ interface GeocodeResult {
24
638
  region?: string;
25
639
  postcode?: string;
26
640
  country?: string;
641
+ /** Unit/suite, where the source published one. Most of the corpus does not. */
642
+ unit?: string;
643
+ };
644
+ /** Structured legal-land parse; only `layer: "legal_land"` results carry it. */
645
+ legal_land?: LegalLandDetails;
646
+ /** Well / facility extras; only `layer: "well" | "facility"` results carry them. */
647
+ uwi?: string;
648
+ licence?: string;
649
+ operator?: string;
650
+ status?: string;
651
+ /** ParcelMap BC PID; only `layer: "parcel"` results carry it. */
652
+ pid?: string;
653
+ /** Survey-cell bounds `[minLng, minLat, maxLng, maxLat]` when the lookup row has a geom. */
654
+ bbox?: [number, number, number, number];
655
+ /** Survey polygon when `?geometry=true` and the lookup row has one. */
656
+ geometry?: {
657
+ type: "Polygon" | "MultiPolygon";
658
+ coordinates: number[][][] | number[][][][];
27
659
  };
28
660
  }
29
661
  interface SearchOptions {
30
662
  limit?: number;
31
663
  lang?: string;
32
- /** [minLng, minLat, maxLng, maxLat] bias/filter. */
664
+ /** [minLng, minLat, maxLng, maxLat] hard filter. Excludes; see `focus` to merely prefer. */
33
665
  bbox?: [number, number, number, number];
666
+ /** [lng, lat] soft proximity preference. Moves ranking toward the point, excludes nothing. */
667
+ focus?: [number, number];
668
+ /** Include the survey polygon on a legal-land hit when the table has one. */
669
+ geometry?: boolean;
670
+ /**
671
+ * Which result types may come back. Civic values (`address`, `street`, `locality`, `region`,
672
+ * `poi`) filter within the civic corpus; energy values (`well`, `facility`) select the energy
673
+ * corpus instead and need the energy add-on. The two sets cannot be mixed.
674
+ */
675
+ layers?: string;
676
+ /** Province or territory, any accepted spelling. Excludes rows in another province. */
677
+ region?: string;
34
678
  signal?: AbortSignal;
35
679
  }
36
680
  interface NearbyOptions {
@@ -62,6 +706,15 @@ declare class Geocoder {
62
706
  search(q: string, opts?: SearchOptions): Promise<GeocodeResult[]>;
63
707
  /** Type-ahead suggestions for partial input. */
64
708
  autocomplete(q: string, opts?: SearchOptions): Promise<GeocodeResult[]>;
709
+ /**
710
+ * Forward geocoding from address components rather than one string.
711
+ *
712
+ * `address` and `city` decide which rows are candidates and which wins. `region` and
713
+ * `postalcode` narrow: they drop rows that contradict them and keep rows that are silent
714
+ * about them, which come back with `region_not_verified` / `postalcode_not_verified` in
715
+ * `match_reasons`. Needs at least one of `address`, `city` or `postalcode`.
716
+ */
717
+ structured(opts: StructuredOptions): Promise<GeocodeResult[]>;
65
718
  /**
66
719
  * Category search: POIs of a kind near a point, sorted by distance (metres, in each
67
720
  * result's `distance`). `category` takes canonical taxonomy ids ("health.pharmacy") or
@@ -74,7 +727,23 @@ declare class Geocoder {
74
727
  lang?: string;
75
728
  signal?: AbortSignal;
76
729
  }): Promise<GeocodeResult[]>;
730
+ /**
731
+ * Which catalog features contain a point. Default layer is `municipalities`
732
+ * (core). `survey.dls` / `survey.nts` need the `legal-land` add-on.
733
+ * `energy.*` need the `energy` add-on. `mining.*` need the `mining` add-on
734
+ * and identify via Land extra tables. Empty until the matching PostGIS table
735
+ * is loaded. No mining search grammar — claim numbers stay `energy.mineral-tenure`.
736
+ */
737
+ identify(opts: IdentifyOptions): Promise<IdentifyResponse>;
738
+ /**
739
+ * Features of one catalog layer that intersect a bbox. Same PostGIS tables as
740
+ * `identify`. Tile-only catalog ids are a 501. Survey layers need `legal-land`;
741
+ * `energy.*` need `energy`; `survey.parcels` needs `agriculture`; mining
742
+ * layers need `mining`. Empty until the matching table is loaded.
743
+ */
744
+ queryLayer(opts: QueryOptions): Promise<QueryResponse>;
745
+ private requestJson;
77
746
  private query;
78
747
  }
79
748
 
80
- export { type GeocodeLayer, type GeocodeResult, Geocoder, GeocoderError, type GeocoderOptions, type NearbyOptions, type SearchOptions };
749
+ export { AMBIGUOUS_SCORE_RATIO, CIVIC_SEARCH_LAYERS, type CivicSearchLayer, CsvParser, type CsvProblem, type CsvRecord, DEFAULT_IDENTIFY_LAYERS, DEFAULT_QUERY_LIMIT, ENERGY_IDENTIFY_LAYER_IDS, ENERGY_SEARCH_LAYERS, type EnergyDetails, type EnergyQuery, type EnergyQueryKind, type EnergySearchLayer, type FocusParse, type GeocodeLayer, type GeocodeMatchReason, type GeocodeMatchType, type GeocodePrecision, type GeocodeResult, type GeocodeSource, Geocoder, GeocoderError, type GeocoderOptions, IDENTIFY_LAYER_IDS, type IdentifyLayerId, type IdentifyOptions, type IdentifyResponse, type IdentifyResult, LAND_IDENTIFY_LAYER_IDS, type LegalLandComponents, type LegalLandDetails, type LegalLandLayerSystem, type LegalLandParse, type LegalLandSystem, type LegalLandUnit, MAX_QUERY_LIMIT, MINING_IDENTIFY_LAYER_IDS, type NearbyOptions, OUTPUT_COLUMNS, OUTPUT_PREFIX, type OutputColumn, type ParcelQuery, QUERY_LAYER_IDS, type QueryBbox, type QueryLayerId, type QueryOptions, type QueryResponse, REGIONS, REGION_CODES, REGION_KEYS, type ReviewReason, type RowStatus, type RuralRouteKind, type RuralRouteQuery, STRUCTURED_FIELDS, type SearchLayersParse, type SearchOptions, type StructuredOptions, type StructuredParseResult, type StructuredQuery, alignRow, classify, encodeField, encodeRow, energySearchRequested, escapeForSpreadsheet, isEnergyIdentifyLayer, isEnergySearchSource, isIdentifyLayer, isLandIdentifyLayer, knownIdentifyLayers, knownQueryLayers, knownSearchLayers, looksLikeEnergyQuery, looksLikeFormula, looksLikeLegalLand, looksLikeParcel, looksLikeRuralRoute, outputValues, parseEnergyQuery, parseEnergySearchLayers, parseFocus, parseIdentifyAt, parseIdentifyLayers, parseLegalLand, parseParcel, parseQueryBbox, parseQueryLayer, parseQueryLimit, parseRegion, parseRuralRoute, parseSearchLayers, parseStructuredQuery, regionForms, resolveOutputHeader, toLegalLandDetails };