@mailwoman/resolver-wof-sqlite 2.1.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.
Files changed (158) hide show
  1. package/README.md +250 -0
  2. package/out/address-point-interpolation.d.ts +48 -0
  3. package/out/address-point-interpolation.d.ts.map +1 -0
  4. package/out/address-point-interpolation.js +164 -0
  5. package/out/address-point-interpolation.js.map +1 -0
  6. package/out/address-point-schema.d.ts +58 -0
  7. package/out/address-point-schema.d.ts.map +1 -0
  8. package/out/address-point-schema.js +67 -0
  9. package/out/address-point-schema.js.map +1 -0
  10. package/out/address-point.d.ts +29 -0
  11. package/out/address-point.d.ts.map +1 -0
  12. package/out/address-point.js +62 -0
  13. package/out/address-point.js.map +1 -0
  14. package/out/ancestry.d.ts +40 -0
  15. package/out/ancestry.d.ts.map +1 -0
  16. package/out/ancestry.js +53 -0
  17. package/out/ancestry.js.map +1 -0
  18. package/out/build-candidate-cli.d.ts +16 -0
  19. package/out/build-candidate-cli.d.ts.map +1 -0
  20. package/out/build-candidate-cli.js +80 -0
  21. package/out/build-candidate-cli.js.map +1 -0
  22. package/out/build-candidate.d.ts +54 -0
  23. package/out/build-candidate.d.ts.map +1 -0
  24. package/out/build-candidate.js +230 -0
  25. package/out/build-candidate.js.map +1 -0
  26. package/out/build-coincident-roles-cli.d.ts +16 -0
  27. package/out/build-coincident-roles-cli.d.ts.map +1 -0
  28. package/out/build-coincident-roles-cli.js +94 -0
  29. package/out/build-coincident-roles-cli.js.map +1 -0
  30. package/out/build-fts-cli.d.ts +23 -0
  31. package/out/build-fts-cli.d.ts.map +1 -0
  32. package/out/build-fts-cli.js +117 -0
  33. package/out/build-fts-cli.js.map +1 -0
  34. package/out/build-slim-cli.d.ts +14 -0
  35. package/out/build-slim-cli.d.ts.map +1 -0
  36. package/out/build-slim-cli.js +130 -0
  37. package/out/build-slim-cli.js.map +1 -0
  38. package/out/build-slim.d.ts +71 -0
  39. package/out/build-slim.d.ts.map +1 -0
  40. package/out/build-slim.js +267 -0
  41. package/out/build-slim.js.map +1 -0
  42. package/out/candidate-lookup.d.ts +43 -0
  43. package/out/candidate-lookup.d.ts.map +1 -0
  44. package/out/candidate-lookup.js +191 -0
  45. package/out/candidate-lookup.js.map +1 -0
  46. package/out/candidate-schema.d.ts +86 -0
  47. package/out/candidate-schema.d.ts.map +1 -0
  48. package/out/candidate-schema.js +109 -0
  49. package/out/candidate-schema.js.map +1 -0
  50. package/out/coincident-roles.d.ts +86 -0
  51. package/out/coincident-roles.d.ts.map +1 -0
  52. package/out/coincident-roles.js +160 -0
  53. package/out/coincident-roles.js.map +1 -0
  54. package/out/convention.d.ts +109 -0
  55. package/out/convention.d.ts.map +1 -0
  56. package/out/convention.js +94 -0
  57. package/out/convention.js.map +1 -0
  58. package/out/fst-autocomplete.d.ts +49 -0
  59. package/out/fst-autocomplete.d.ts.map +1 -0
  60. package/out/fst-autocomplete.js +124 -0
  61. package/out/fst-autocomplete.js.map +1 -0
  62. package/out/fst-builder.d.ts +20 -0
  63. package/out/fst-builder.d.ts.map +1 -0
  64. package/out/fst-builder.js +219 -0
  65. package/out/fst-builder.js.map +1 -0
  66. package/out/fst-deserialize-web.d.ts +16 -0
  67. package/out/fst-deserialize-web.d.ts.map +1 -0
  68. package/out/fst-deserialize-web.js +133 -0
  69. package/out/fst-deserialize-web.js.map +1 -0
  70. package/out/fst-matcher.d.ts +33 -0
  71. package/out/fst-matcher.d.ts.map +1 -0
  72. package/out/fst-matcher.js +117 -0
  73. package/out/fst-matcher.js.map +1 -0
  74. package/out/fst-serialize.d.ts +30 -0
  75. package/out/fst-serialize.d.ts.map +1 -0
  76. package/out/fst-serialize.js +261 -0
  77. package/out/fst-serialize.js.map +1 -0
  78. package/out/fst-types.d.ts +60 -0
  79. package/out/fst-types.d.ts.map +1 -0
  80. package/out/fst-types.js +11 -0
  81. package/out/fst-types.js.map +1 -0
  82. package/out/fts.d.ts +158 -0
  83. package/out/fts.d.ts.map +1 -0
  84. package/out/fts.js +261 -0
  85. package/out/fts.js.map +1 -0
  86. package/out/geo.d.ts +74 -0
  87. package/out/geo.d.ts.map +1 -0
  88. package/out/geo.js +88 -0
  89. package/out/geo.js.map +1 -0
  90. package/out/index.d.ts +27 -0
  91. package/out/index.d.ts.map +1 -0
  92. package/out/index.js +22 -0
  93. package/out/index.js.map +1 -0
  94. package/out/interpolation.d.ts +84 -0
  95. package/out/interpolation.d.ts.map +1 -0
  96. package/out/interpolation.js +150 -0
  97. package/out/interpolation.js.map +1 -0
  98. package/out/lookup.d.ts +156 -0
  99. package/out/lookup.d.ts.map +1 -0
  100. package/out/lookup.js +876 -0
  101. package/out/lookup.js.map +1 -0
  102. package/out/postal-city-alias-lookup.d.ts +50 -0
  103. package/out/postal-city-alias-lookup.d.ts.map +1 -0
  104. package/out/postal-city-alias-lookup.js +66 -0
  105. package/out/postal-city-alias-lookup.js.map +1 -0
  106. package/out/postal-city-alias-schema.d.ts +51 -0
  107. package/out/postal-city-alias-schema.d.ts.map +1 -0
  108. package/out/postal-city-alias-schema.js +47 -0
  109. package/out/postal-city-alias-schema.js.map +1 -0
  110. package/out/postal-city-candidate-schema.d.ts +58 -0
  111. package/out/postal-city-candidate-schema.d.ts.map +1 -0
  112. package/out/postal-city-candidate-schema.js +56 -0
  113. package/out/postal-city-candidate-schema.js.map +1 -0
  114. package/out/postcode-point-lookup.d.ts +38 -0
  115. package/out/postcode-point-lookup.d.ts.map +1 -0
  116. package/out/postcode-point-lookup.js +46 -0
  117. package/out/postcode-point-lookup.js.map +1 -0
  118. package/out/reverse.d.ts +99 -0
  119. package/out/reverse.d.ts.map +1 -0
  120. package/out/reverse.js +290 -0
  121. package/out/reverse.js.map +1 -0
  122. package/out/schema.d.ts +163 -0
  123. package/out/schema.d.ts.map +1 -0
  124. package/out/schema.js +18 -0
  125. package/out/schema.js.map +1 -0
  126. package/out/sharding.d.ts +96 -0
  127. package/out/sharding.d.ts.map +1 -0
  128. package/out/sharding.js +129 -0
  129. package/out/sharding.js.map +1 -0
  130. package/out/sqlite-convention-source.d.ts +29 -0
  131. package/out/sqlite-convention-source.d.ts.map +1 -0
  132. package/out/sqlite-convention-source.js +53 -0
  133. package/out/sqlite-convention-source.js.map +1 -0
  134. package/out/sqlite-utils.d.ts +17 -0
  135. package/out/sqlite-utils.d.ts.map +1 -0
  136. package/out/sqlite-utils.js +24 -0
  137. package/out/sqlite-utils.js.map +1 -0
  138. package/out/street-morphology-fst-builder.d.ts +59 -0
  139. package/out/street-morphology-fst-builder.d.ts.map +1 -0
  140. package/out/street-morphology-fst-builder.js +174 -0
  141. package/out/street-morphology-fst-builder.js.map +1 -0
  142. package/out/street-normalize.d.ts +66 -0
  143. package/out/street-normalize.d.ts.map +1 -0
  144. package/out/street-normalize.js +176 -0
  145. package/out/street-normalize.js.map +1 -0
  146. package/out/street-segment-schema.d.ts +61 -0
  147. package/out/street-segment-schema.d.ts.map +1 -0
  148. package/out/street-segment-schema.js +64 -0
  149. package/out/street-segment-schema.js.map +1 -0
  150. package/out/types.d.ts +137 -0
  151. package/out/types.d.ts.map +1 -0
  152. package/out/types.js +13 -0
  153. package/out/types.js.map +1 -0
  154. package/out/unified-schema.d.ts +25 -0
  155. package/out/unified-schema.d.ts.map +1 -0
  156. package/out/unified-schema.js +142 -0
  157. package/out/unified-schema.js.map +1 -0
  158. package/package.json +54 -0
@@ -0,0 +1,109 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Typed schema for the byte-range CANDIDATE gazetteer (`candidate.db`) — the single source of truth
7
+ * for the columns shared by the BUILDER ({@link buildCandidateTable}) and the READERS (the Node
8
+ * {@link WofCandidateTableLookup} + the browser `httpvfs-resolver.ts`). Before this module each
9
+ * side hand-wrote the column list; a rename in one place broke the other at runtime. Now the
10
+ * contract is a Kysely `Database` interface (`new DatabaseClient<CandidateDatabase>(...)` for
11
+ * typed inserts) plus the table DDL as strings — so a column change is a compile error on every
12
+ * consumer.
13
+ *
14
+ * `cand_stage` is the transient staging table the builder bulk-loads; `candidate` is the clustered
15
+ * `WITHOUT ROWID` B-tree it's materialized into (same columns). The reader queries `candidate`.
16
+ */
17
+ import { sql } from "kysely";
18
+ /**
19
+ * The `candidate`/`cand_stage` columns in clustered-key order. The materialization `INSERT INTO
20
+ * candidate SELECT … FROM cand_stage` derives its column list from this, so the two tables can't
21
+ * drift. Keep in sync with {@link CandidateTable}.
22
+ */
23
+ export const CANDIDATE_COLUMNS = [
24
+ "name_key",
25
+ "country_id",
26
+ "region_id",
27
+ "placetype_id",
28
+ "neg_rank",
29
+ "spr_id",
30
+ "name",
31
+ "latitude",
32
+ "longitude",
33
+ "min_lat",
34
+ "min_lon",
35
+ "max_lat",
36
+ "max_lon",
37
+ "population",
38
+ "is_primary",
39
+ ];
40
+ /**
41
+ * Create the code dictionaries + the transient staging table — called before the build's load
42
+ * passes. `cand_stage` mirrors {@link CandidateTable} but every column is nullable (the loader fills
43
+ * them positionally). Pass a {@link DatabaseClient} (or any `Kysely`) over the candidate DB.
44
+ */
45
+ export async function createCandidateStagingTables(db) {
46
+ await db.schema
47
+ .createTable("country_codes")
48
+ .addColumn("id", "integer", (c) => c.primaryKey())
49
+ .addColumn("code", "text", (c) => c.unique())
50
+ .execute();
51
+ await db.schema
52
+ .createTable("placetype_codes")
53
+ .addColumn("id", "integer", (c) => c.primaryKey())
54
+ .addColumn("placetype", "text", (c) => c.unique())
55
+ .execute();
56
+ await db.schema
57
+ .createTable("cand_stage")
58
+ .addColumn("name_key", "text")
59
+ .addColumn("country_id", "integer")
60
+ .addColumn("region_id", "integer")
61
+ .addColumn("placetype_id", "integer")
62
+ .addColumn("neg_rank", "real")
63
+ .addColumn("spr_id", "integer")
64
+ .addColumn("name", "text")
65
+ .addColumn("latitude", "real")
66
+ .addColumn("longitude", "real")
67
+ .addColumn("min_lat", "real")
68
+ .addColumn("min_lon", "real")
69
+ .addColumn("max_lat", "real")
70
+ .addColumn("max_lon", "real")
71
+ .addColumn("population", "integer")
72
+ .addColumn("is_primary", "integer")
73
+ .execute();
74
+ }
75
+ /**
76
+ * Create the clustered `WITHOUT ROWID` lookup table — called after staging, before the VACUUM. The
77
+ * first six columns form the clustered primary key (population-ranked via `neg_rank`).
78
+ */
79
+ export async function createCandidateTable(db) {
80
+ await db.schema
81
+ .createTable("candidate")
82
+ .addColumn("name_key", "text", (c) => c.notNull())
83
+ .addColumn("country_id", "integer", (c) => c.notNull())
84
+ .addColumn("region_id", "integer", (c) => c.notNull())
85
+ .addColumn("placetype_id", "integer", (c) => c.notNull())
86
+ .addColumn("neg_rank", "real", (c) => c.notNull())
87
+ .addColumn("spr_id", "integer", (c) => c.notNull())
88
+ .addColumn("name", "text")
89
+ .addColumn("latitude", "real")
90
+ .addColumn("longitude", "real")
91
+ .addColumn("min_lat", "real")
92
+ .addColumn("min_lon", "real")
93
+ .addColumn("max_lat", "real")
94
+ .addColumn("max_lon", "real")
95
+ .addColumn("population", "integer")
96
+ .addColumn("is_primary", "integer")
97
+ .addPrimaryKeyConstraint("candidate_pk", [
98
+ "name_key",
99
+ "country_id",
100
+ "region_id",
101
+ "placetype_id",
102
+ "neg_rank",
103
+ "spr_id",
104
+ ])
105
+ // `WITHOUT ROWID` has no first-class builder; the raw modifier is the idiomatic fallback.
106
+ .modifyEnd(sql `without rowid`)
107
+ .execute();
108
+ }
109
+ //# sourceMappingURL=candidate-schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"candidate-schema.js","sourceRoot":"","sources":["../candidate-schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `buildCoincidentRoles` — derives the **coincident-roles relation** (#403, epic #402) into the
7
+ * unified gazetteer.
8
+ *
9
+ * Many places occupy MULTIPLE admin tiers under one name: German city-states (Berlin/Hamburg/Bremen
10
+ * = city == state), Italian provinces named after their capital (Milano, Varese…), Spanish
11
+ * provinces-after-capitals, UK unitary authorities, JP prefectures, NL province-capitals
12
+ * (Utrecht/Groningen), Shanghai. When an address surfaces only the admin role (the parser drops
13
+ * the locality span), the resolver has no locality to place. The hierarchy-completion step (#405)
14
+ * repairs that by consulting THIS relation; the table replaces #387's hardcoded 15 km constant
15
+ * with the gazetteer's own structure, so the runtime is an O(1) membership lookup with no
16
+ * distance math.
17
+ *
18
+ * V1 is REGION-tier only (admin.placetype = `region`): the ~124 places matching the census across 9
19
+ * countries (IT/ES/GB/JP/KR/FR/DE/NL/CN). County-tier same-name coincidences are deliberately
20
+ * excluded — they're dominated by French cantons and JP counties (admin subdivisions named after
21
+ * a seat town, not dual-role cities) that don't hit the parser-drops-locality failure; genuine
22
+ * consolidated city-counties (US SF/Denver) are a separate follow-up needing a relative-size
23
+ * filter.
24
+ *
25
+ * A pair `(admin, locality)` is recorded when all hold: same `name` (case-insensitive), the
26
+ * locality is a `descendant` of the admin (via the `ancestors` table), and their centroids are
27
+ * within a RELATIVE tolerance — `toleranceFraction × admin-bbox-diagonal`, floored at
28
+ * `minToleranceKm`. The relative term lets a large Italian province admit a city ~tens of km from
29
+ * its centroid while a tiny city-state stays tight; the floor catches city-states whose bbox is
30
+ * small (Bremen's centroids sit 9.3 km apart). The tolerance lives ONLY here at build time — it
31
+ * never enters the resolver hot path.
32
+ *
33
+ * `relationship_type` is recorded for debuggability / deferred per-type behavior; v1 completion is
34
+ * uniform (see #405). It's a coarse classification, not critical.
35
+ *
36
+ * Mirrors the derived-table builder pattern in `fts.ts` (`buildPlaceSearchFts`). Run incrementally
37
+ * against an existing `admin-global-priority.db` via `build-coincident-roles-cli.ts`; should also
38
+ * be wired as a post-step of the main `scripts/build-unified-wof.ts`.
39
+ */
40
+ import type { DatabaseSync } from "node:sqlite";
41
+ export declare const COINCIDENT_ROLES_TABLE = "coincident_roles";
42
+ /** A place that plays multiple admin roles — one row of the relation, keyed by `admin_id`. */
43
+ export interface CoincidentRole {
44
+ localityId: number;
45
+ relationshipType: "city-state" | "capital-seat" | "consolidated-county";
46
+ adminPlacetype: string;
47
+ distanceKm: number;
48
+ population: number;
49
+ }
50
+ export interface BuildCoincidentRolesOpts {
51
+ /** Drop + rebuild the table if it already exists. Default true (the build is cheap + idempotent). */
52
+ drop?: boolean;
53
+ /**
54
+ * Relative tolerance: a pair is kept when centroid distance ≤ `toleranceFraction ×
55
+ * bbox-diagonal`. Default 0.15.
56
+ */
57
+ toleranceFraction?: number;
58
+ /** Floor (km) under the relative tolerance, so small-bbox city-states still qualify. Default 12. */
59
+ minToleranceKm?: number;
60
+ /**
61
+ * Centroid distance (km) below which a region-tier pair is classed `city-state` (metadata only).
62
+ * Default 2.
63
+ */
64
+ cityStateMaxKm?: number;
65
+ onProgress?: (phase: string, detail?: string) => void;
66
+ }
67
+ export interface BuildCoincidentRolesResult {
68
+ created: boolean;
69
+ rowCount: number;
70
+ byCountry: Record<string, number>;
71
+ durationMs: number;
72
+ }
73
+ /**
74
+ * Derive the coincident-roles relation into `db`. Additive — only creates/replaces the
75
+ * `coincident_roles` table; never touches `spr`/`names`/`ancestors`. Idempotent.
76
+ */
77
+ export declare function buildCoincidentRoles(db: DatabaseSync, opts?: BuildCoincidentRolesOpts): BuildCoincidentRolesResult;
78
+ /** True iff the relation table exists. Used by the resolver to decide whether completion can run. */
79
+ export declare function coincidentRolesExists(db: DatabaseSync): boolean;
80
+ /**
81
+ * Load the relation into an in-memory map keyed by `admin_id` for O(1) runtime lookup (#405). Each
82
+ * admin may map to MULTIPLE same-name descendants; the consumer disambiguates (min distance →
83
+ * population → abstain). Returns an empty map when the table is absent.
84
+ */
85
+ export declare function loadCoincidentRoles(db: DatabaseSync): Map<number, CoincidentRole[]>;
86
+ //# sourceMappingURL=coincident-roles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coincident-roles.d.ts","sourceRoot":"","sources":["../coincident-roles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE/C,eAAO,MAAM,sBAAsB,qBAAqB,CAAA;AAExD,8FAA8F;AAC9F,MAAM,WAAW,cAAc;IAC9B,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB,EAAE,YAAY,GAAG,cAAc,GAAG,qBAAqB,CAAA;IACvE,cAAc,EAAE,MAAM,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,wBAAwB;IACxC,qGAAqG;IACrG,IAAI,CAAC,EAAE,OAAO,CAAA;IACd;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,oGAAoG;IACpG,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CACrD;AAED,MAAM,WAAW,0BAA0B;IAC1C,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACjC,UAAU,EAAE,MAAM,CAAA;CAClB;AAsBD;;;GAGG;AACH,wBAAgB,oBAAoB,CACnC,EAAE,EAAE,YAAY,EAChB,IAAI,GAAE,wBAA6B,GACjC,0BAA0B,CAmF5B;AAED,qGAAqG;AACrG,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,YAAY,GAAG,OAAO,CAE/D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC,CA6BnF"}
@@ -0,0 +1,160 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `buildCoincidentRoles` — derives the **coincident-roles relation** (#403, epic #402) into the
7
+ * unified gazetteer.
8
+ *
9
+ * Many places occupy MULTIPLE admin tiers under one name: German city-states (Berlin/Hamburg/Bremen
10
+ * = city == state), Italian provinces named after their capital (Milano, Varese…), Spanish
11
+ * provinces-after-capitals, UK unitary authorities, JP prefectures, NL province-capitals
12
+ * (Utrecht/Groningen), Shanghai. When an address surfaces only the admin role (the parser drops
13
+ * the locality span), the resolver has no locality to place. The hierarchy-completion step (#405)
14
+ * repairs that by consulting THIS relation; the table replaces #387's hardcoded 15 km constant
15
+ * with the gazetteer's own structure, so the runtime is an O(1) membership lookup with no
16
+ * distance math.
17
+ *
18
+ * V1 is REGION-tier only (admin.placetype = `region`): the ~124 places matching the census across 9
19
+ * countries (IT/ES/GB/JP/KR/FR/DE/NL/CN). County-tier same-name coincidences are deliberately
20
+ * excluded — they're dominated by French cantons and JP counties (admin subdivisions named after
21
+ * a seat town, not dual-role cities) that don't hit the parser-drops-locality failure; genuine
22
+ * consolidated city-counties (US SF/Denver) are a separate follow-up needing a relative-size
23
+ * filter.
24
+ *
25
+ * A pair `(admin, locality)` is recorded when all hold: same `name` (case-insensitive), the
26
+ * locality is a `descendant` of the admin (via the `ancestors` table), and their centroids are
27
+ * within a RELATIVE tolerance — `toleranceFraction × admin-bbox-diagonal`, floored at
28
+ * `minToleranceKm`. The relative term lets a large Italian province admit a city ~tens of km from
29
+ * its centroid while a tiny city-state stays tight; the floor catches city-states whose bbox is
30
+ * small (Bremen's centroids sit 9.3 km apart). The tolerance lives ONLY here at build time — it
31
+ * never enters the resolver hot path.
32
+ *
33
+ * `relationship_type` is recorded for debuggability / deferred per-type behavior; v1 completion is
34
+ * uniform (see #405). It's a coarse classification, not critical.
35
+ *
36
+ * Mirrors the derived-table builder pattern in `fts.ts` (`buildPlaceSearchFts`). Run incrementally
37
+ * against an existing `admin-global-priority.db` via `build-coincident-roles-cli.ts`; should also
38
+ * be wired as a post-step of the main `scripts/build-unified-wof.ts`.
39
+ */
40
+ import { haversineKm } from "@mailwoman/spatial";
41
+ export const COINCIDENT_ROLES_TABLE = "coincident_roles";
42
+ function tableExists(db, name) {
43
+ return !!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?").get(name);
44
+ }
45
+ /**
46
+ * Derive the coincident-roles relation into `db`. Additive — only creates/replaces the
47
+ * `coincident_roles` table; never touches `spr`/`names`/`ancestors`. Idempotent.
48
+ */
49
+ export function buildCoincidentRoles(db, opts = {}) {
50
+ const start = Date.now();
51
+ const drop = opts.drop ?? true;
52
+ const toleranceFraction = opts.toleranceFraction ?? 0.15;
53
+ const minToleranceKm = opts.minToleranceKm ?? 12;
54
+ const cityStateMaxKm = opts.cityStateMaxKm ?? 2;
55
+ const onProgress = opts.onProgress ?? (() => { });
56
+ if (tableExists(db, COINCIDENT_ROLES_TABLE) && drop) {
57
+ onProgress("dropping", COINCIDENT_ROLES_TABLE);
58
+ db.exec(`DROP TABLE ${COINCIDENT_ROLES_TABLE}`);
59
+ }
60
+ onProgress("creating", COINCIDENT_ROLES_TABLE);
61
+ // Raw DDL by design: this is a sync builder consumed by a sync CLI (build-coincident-roles-cli) and
62
+ // 6 sync unit tests, so routing one table through async Kysely would cascade async through all of
63
+ // them for no real gain. See AGENTS.md "Database / inline SQL". (The SELECT + INSERT loop below are
64
+ // likewise the raw hot path.)
65
+ db.exec(`
66
+ CREATE TABLE IF NOT EXISTS ${COINCIDENT_ROLES_TABLE} (
67
+ admin_id INTEGER NOT NULL,
68
+ locality_id INTEGER NOT NULL,
69
+ relationship_type TEXT NOT NULL,
70
+ admin_placetype TEXT NOT NULL,
71
+ distance_km REAL NOT NULL,
72
+ locality_population INTEGER NOT NULL DEFAULT 0,
73
+ PRIMARY KEY (admin_id, locality_id)
74
+ )
75
+ `);
76
+ onProgress("scanning");
77
+ // Admin (region/county tier) ⋈ same-name DESCENDANT locality. `place_population` is optional (LEFT
78
+ // JOIN → 0 when absent). The relative-tolerance filter + relationship classification happen in JS so
79
+ // the SQL stays a plain join. `spr` exposes the bbox columns we need for the diagonal.
80
+ const candidates = db
81
+ .prepare(`SELECT r.id AS admin_id, r.placetype AS admin_placetype, r.country AS country, l.id AS locality_id,
82
+ r.latitude AS rlat, r.longitude AS rlon, l.latitude AS llat, l.longitude AS llon,
83
+ r.min_latitude, r.min_longitude, r.max_latitude, r.max_longitude,
84
+ COALESCE(p.population, 0) AS pop
85
+ FROM spr r
86
+ JOIN spr l ON lower(l.name) = lower(r.name) AND l.placetype = 'locality'
87
+ AND l.is_current != 0 AND l.is_deprecated = 0
88
+ JOIN ${"ancestors"} a ON a.id = l.id AND a.ancestor_id = r.id
89
+ LEFT JOIN place_population p ON p.id = l.id
90
+ WHERE r.placetype = 'region'
91
+ AND r.is_current != 0 AND r.is_deprecated = 0`)
92
+ .all();
93
+ onProgress("filtering", `${candidates.length} candidates`);
94
+ const insert = db.prepare(`INSERT OR REPLACE INTO ${COINCIDENT_ROLES_TABLE}
95
+ (admin_id, locality_id, relationship_type, admin_placetype, distance_km, locality_population)
96
+ VALUES (?, ?, ?, ?, ?, ?)`);
97
+ const byCountry = {};
98
+ let rowCount = 0;
99
+ db.exec("BEGIN");
100
+ try {
101
+ for (const c of candidates) {
102
+ const dist = haversineKm(c.rlat, c.rlon, c.llat, c.llon);
103
+ const diag = haversineKm(c.min_latitude, c.min_longitude, c.max_latitude, c.max_longitude);
104
+ const tolerance = Math.max(toleranceFraction * diag, minToleranceKm);
105
+ if (dist > tolerance)
106
+ continue;
107
+ // v1 is region-tier only: a place is a `city-state` when its centroid coincides with the
108
+ // region's (Berlin/Hamburg), else `capital-seat` (a region named after its principal city, e.g.
109
+ // Milano province → Milano comune). `consolidated-county` is reserved for a future county-tier
110
+ // pass (US SF/Denver) — excluded from v1 because county-tier same-name coincidences are
111
+ // dominated by French cantons / JP counties that don't hit the parser-drops-locality failure.
112
+ const relationshipType = dist <= cityStateMaxKm ? "city-state" : "capital-seat";
113
+ insert.run(c.admin_id, c.locality_id, relationshipType, c.admin_placetype, dist, c.pop);
114
+ rowCount++;
115
+ byCountry[c.country] = (byCountry[c.country] ?? 0) + 1;
116
+ }
117
+ db.exec("COMMIT");
118
+ }
119
+ catch (err) {
120
+ db.exec("ROLLBACK");
121
+ throw err;
122
+ }
123
+ db.exec(`CREATE INDEX IF NOT EXISTS coincident_roles_by_admin ON ${COINCIDENT_ROLES_TABLE} (admin_id)`);
124
+ onProgress("done", `${rowCount} coincident-role rows`);
125
+ return { created: true, rowCount, byCountry, durationMs: Date.now() - start };
126
+ }
127
+ /** True iff the relation table exists. Used by the resolver to decide whether completion can run. */
128
+ export function coincidentRolesExists(db) {
129
+ return tableExists(db, COINCIDENT_ROLES_TABLE);
130
+ }
131
+ /**
132
+ * Load the relation into an in-memory map keyed by `admin_id` for O(1) runtime lookup (#405). Each
133
+ * admin may map to MULTIPLE same-name descendants; the consumer disambiguates (min distance →
134
+ * population → abstain). Returns an empty map when the table is absent.
135
+ */
136
+ export function loadCoincidentRoles(db) {
137
+ const map = new Map();
138
+ if (!coincidentRolesExists(db))
139
+ return map;
140
+ const rows = db
141
+ .prepare(`SELECT admin_id, locality_id, relationship_type, admin_placetype, distance_km, locality_population
142
+ FROM ${COINCIDENT_ROLES_TABLE}`)
143
+ .all();
144
+ for (const r of rows) {
145
+ const entry = {
146
+ localityId: r.locality_id,
147
+ relationshipType: r.relationship_type,
148
+ adminPlacetype: r.admin_placetype,
149
+ distanceKm: r.distance_km,
150
+ population: r.locality_population,
151
+ };
152
+ const list = map.get(r.admin_id);
153
+ if (list)
154
+ list.push(entry);
155
+ else
156
+ map.set(r.admin_id, [entry]);
157
+ }
158
+ return map;
159
+ }
160
+ //# sourceMappingURL=coincident-roles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coincident-roles.js","sourceRoot":"","sources":["../coincident-roles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAGhD,MAAM,CAAC,MAAM,sBAAsB,GAAG,kBAAkB,CAAA;AAoDxD,SAAS,WAAW,CAAC,EAAgB,EAAE,IAAY;IAClD,OAAO,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,6DAA6D,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;AAC7F,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CACnC,EAAgB,EAChB,OAAiC,EAAE;IAEnC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IACxB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,IAAI,CAAA;IAC9B,MAAM,iBAAiB,GAAG,IAAI,CAAC,iBAAiB,IAAI,IAAI,CAAA;IACxD,MAAM,cAAc,GAAG,IAAI,CAAC,cAAc,IAAI,EAAE,CAAA;IAChD,MAAM,cAAc,GAAG,IAAI,CAAC,cAAc,IAAI,CAAC,CAAA;IAC/C,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;IAEhD,IAAI,WAAW,CAAC,EAAE,EAAE,sBAAsB,CAAC,IAAI,IAAI,EAAE,CAAC;QACrD,UAAU,CAAC,UAAU,EAAE,sBAAsB,CAAC,CAAA;QAC9C,EAAE,CAAC,IAAI,CAAC,cAAc,sBAAsB,EAAE,CAAC,CAAA;IAChD,CAAC;IACD,UAAU,CAAC,UAAU,EAAE,sBAAsB,CAAC,CAAA;IAC9C,oGAAoG;IACpG,kGAAkG;IAClG,oGAAoG;IACpG,8BAA8B;IAC9B,EAAE,CAAC,IAAI,CAAC;+BACsB,sBAAsB;;;;;;;;;EASnD,CAAC,CAAA;IAEF,UAAU,CAAC,UAAU,CAAC,CAAA;IACtB,mGAAmG;IACnG,qGAAqG;IACrG,uFAAuF;IACvF,MAAM,UAAU,GAAG,EAAE;SACnB,OAAO,CACP;;;;;;;UAOO,WAAW;;;kDAG6B,CAC/C;SACA,GAAG,EAA+B,CAAA;IAEpC,UAAU,CAAC,WAAW,EAAE,GAAG,UAAU,CAAC,MAAM,aAAa,CAAC,CAAA;IAC1D,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CACxB,0BAA0B,sBAAsB;;6BAErB,CAC3B,CAAA;IACD,MAAM,SAAS,GAA2B,EAAE,CAAA;IAC5C,IAAI,QAAQ,GAAG,CAAC,CAAA;IAChB,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAChB,IAAI,CAAC;QACJ,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;YAC5B,MAAM,IAAI,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAA;YACxD,MAAM,IAAI,GAAG,WAAW,CAAC,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC,aAAa,CAAC,CAAA;YAC1F,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,iBAAiB,GAAG,IAAI,EAAE,cAAc,CAAC,CAAA;YACpE,IAAI,IAAI,GAAG,SAAS;gBAAE,SAAQ;YAC9B,yFAAyF;YACzF,gGAAgG;YAChG,+FAA+F;YAC/F,wFAAwF;YACxF,8FAA8F;YAC9F,MAAM,gBAAgB,GAAG,IAAI,IAAI,cAAc,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,cAAc,CAAA;YAC/E,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,WAAW,EAAE,gBAAgB,EAAE,CAAC,CAAC,eAAe,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAA;YACvF,QAAQ,EAAE,CAAA;YACV,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAA;QACvD,CAAC;QACD,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IAClB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;QACnB,MAAM,GAAG,CAAA;IACV,CAAC;IACD,EAAE,CAAC,IAAI,CAAC,2DAA2D,sBAAsB,aAAa,CAAC,CAAA;IAEvG,UAAU,CAAC,MAAM,EAAE,GAAG,QAAQ,uBAAuB,CAAC,CAAA;IACtD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE,CAAA;AAC9E,CAAC;AAED,qGAAqG;AACrG,MAAM,UAAU,qBAAqB,CAAC,EAAgB;IACrD,OAAO,WAAW,CAAC,EAAE,EAAE,sBAAsB,CAAC,CAAA;AAC/C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,EAAgB;IACnD,MAAM,GAAG,GAAG,IAAI,GAAG,EAA4B,CAAA;IAC/C,IAAI,CAAC,qBAAqB,CAAC,EAAE,CAAC;QAAE,OAAO,GAAG,CAAA;IAC1C,MAAM,IAAI,GAAG,EAAE;SACb,OAAO,CACP;UACO,sBAAsB,EAAE,CAC/B;SACA,GAAG,EAOH,CAAA;IACF,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACtB,MAAM,KAAK,GAAmB;YAC7B,UAAU,EAAE,CAAC,CAAC,WAAW;YACzB,gBAAgB,EAAE,CAAC,CAAC,iBAAiB;YACrC,cAAc,EAAE,CAAC,CAAC,eAAe;YACjC,UAAU,EAAE,CAAC,CAAC,WAAW;YACzB,UAAU,EAAE,CAAC,CAAC,mBAAmB;SACjC,CAAA;QACD,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAA;QAChC,IAAI,IAAI;YAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;;YACrB,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC,CAAA;IAClC,CAAC;IACD,OAAO,GAAG,CAAA;AACX,CAAC"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The **Geographic Rule Engine** convention model (Direction E, #289 — see
7
+ * `docs/articles/plan/2026-06-05-geographic-rule-engine.md` and epic #288).
8
+ *
9
+ * A `Convention` is a declarative resolution profile attached to a Who's-On-First admin polygon.
10
+ * The engine deep-merges the conventions along a resolved place's ancestor chain — country →
11
+ * region → … → locality, most-specific winning — and the backend dispatches the named strategies
12
+ * in `candidateStrategies`, first to return candidates wins.
13
+ *
14
+ * This module is the backend-agnostic core: the convention TYPES, the deep-merge, and the seed
15
+ * source. The strategy IMPLEMENTATIONS are SQL-bound and live in `lookup.ts`, registered by
16
+ * name.
17
+ *
18
+ * For the existing EU locales (DE/FR/GB/NL) the seed source is empty, so every query resolves to
19
+ * `WORLD_DEFAULT` and the dispatch is byte-identical to the pre-engine coordinate-first path. JP
20
+ * / KR / TW add rows here (and #290 swaps the seed map for a build-from-source sqlite-backed
21
+ * source).
22
+ */
23
+ import type { FindPlaceQuery, PlaceCandidate } from "./types.js";
24
+ /**
25
+ * Soft-scoring weights for the `postcode_area_resolution` strategy: `pc·S_pc + name·S_name +
26
+ * pop·S_pop`.
27
+ */
28
+ export interface ScoringWeights {
29
+ pc: number;
30
+ name: number;
31
+ pop: number;
32
+ }
33
+ /**
34
+ * A geographically-scoped resolution profile. Namespaced sections grow per phase; #289 ships the
35
+ * dispatch + scoring slice (`candidateStrategies` + `scoringWeights`). Later phases add
36
+ * `fieldMapping` (locale semantics for `locator[]`), `tokenNormalization`, etc.
37
+ */
38
+ export interface Convention {
39
+ /** Ordered strategy names the dispatcher runs; the first to return a non-null result wins. */
40
+ candidateStrategies?: string[];
41
+ /**
42
+ * Weights for `postcode_area_resolution`'s soft-score. Partial — a layer may nudge one weight and
43
+ * inherit the rest from the layers below it (`resolveConvention` fills any gaps from
44
+ * WORLD_DEFAULT).
45
+ */
46
+ scoringWeights?: Partial<ScoringWeights>;
47
+ }
48
+ /**
49
+ * A fully-resolved convention: every field present, weights complete. What `resolveConvention`
50
+ * returns and what strategies consume.
51
+ */
52
+ export interface ResolvedConvention {
53
+ candidateStrategies: string[];
54
+ scoringWeights: ScoringWeights;
55
+ }
56
+ /**
57
+ * The base layer every ancestor chain starts from. Reproduces the pre-engine coordinate-first
58
+ * behavior exactly: try `postcode_area_resolution`, else fall back to fuzzy name match; soft-score
59
+ * weights 0.6 / 0.3 / 0.1. Changing these changes EU behavior — don't, without a byte-stability
60
+ * run.
61
+ */
62
+ export declare const WORLD_DEFAULT: ResolvedConvention;
63
+ /**
64
+ * The strategy names the backend registers. The single source of truth shared by the dispatch
65
+ * registry and the build-time validator, so an authored convention that names a non-existent
66
+ * strategy is caught at build (loud) rather than silently skipped at runtime.
67
+ */
68
+ export declare const BUILTIN_STRATEGY_NAMES: readonly ["postcode_area_resolution", "fallback_fuzzy_name_match"];
69
+ /**
70
+ * Table name for the convention asset (#290). Carried here so the build script, the runtime source,
71
+ * and the shard auto-detect all agree.
72
+ */
73
+ export declare const ADDRESS_CONVENTION_TABLE = "address_convention";
74
+ /**
75
+ * A named resolution primitive. Returns `null` to abstain (gate unmet / no data) → the dispatcher
76
+ * tries the next strategy; returns an array (possibly empty) to claim the result.
77
+ */
78
+ export type Strategy = (query: FindPlaceQuery, convention: ResolvedConvention) => Promise<PlaceCandidate[] | null>;
79
+ /**
80
+ * Look up a convention record by WOF polygon id. Returns `undefined` when the polygon has no
81
+ * override.
82
+ */
83
+ export interface ConventionSource {
84
+ get(wofId: number): Convention | undefined;
85
+ }
86
+ /**
87
+ * In-memory convention source seeded from a `{ wofId: Convention }` map. Empty for the EU locales
88
+ * (they ride `WORLD_DEFAULT`); JP / KR / TW add rows. #290 replaces this with a sqlite-backed
89
+ * source built from source, same distributable-asset discipline as `postcode-locality-intl.db`.
90
+ */
91
+ export declare class SeedConventionSource implements ConventionSource {
92
+ #private;
93
+ constructor(rows?: Record<number, Convention>);
94
+ get(wofId: number): Convention | undefined;
95
+ }
96
+ /**
97
+ * Deep-merge convention layers, later (more-specific) layers winning per field.
98
+ * `candidateStrategies` is replaced wholesale — a convention names its full ordered list, it does
99
+ * not append. `scoringWeights` is merged key-by-key so a locality can nudge one weight without
100
+ * restating the others.
101
+ */
102
+ export declare function mergeConventions(base: Convention, ...overrides: Array<Convention | undefined>): Convention;
103
+ /**
104
+ * Resolve the effective convention for a place given its ancestor chain, ordered MOST-GENERAL →
105
+ * MOST-SPECIFIC (country, region, …, locality). Starts from `WORLD_DEFAULT` so every field is
106
+ * defined regardless of which (if any) ancestors carry an override.
107
+ */
108
+ export declare function resolveConvention(source: ConventionSource, ancestorIds: readonly number[]): ResolvedConvention;
109
+ //# sourceMappingURL=convention.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convention.d.ts","sourceRoot":"","sources":["../convention.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEhE;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;CACX;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IAC1B,8FAA8F;IAC9F,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC9B;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAA;CACxC;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IAClC,mBAAmB,EAAE,MAAM,EAAE,CAAA;IAC7B,cAAc,EAAE,cAAc,CAAA;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,aAAa,EAAE,kBAG3B,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,oEAAqE,CAAA;AAExG;;;GAGG;AACH,eAAO,MAAM,wBAAwB,uBAAuB,CAAA;AAE5D;;;GAGG;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,kBAAkB,KAAK,OAAO,CAAC,cAAc,EAAE,GAAG,IAAI,CAAC,CAAA;AAElH;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAChC,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAA;CAC1C;AAED;;;;GAIG;AACH,qBAAa,oBAAqB,YAAW,gBAAgB;;gBAGhD,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAM;IAIjD,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;CAG1C;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,SAAS,EAAE,KAAK,CAAC,UAAU,GAAG,SAAS,CAAC,GAAG,UAAU,CAa1G;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,gBAAgB,EAAE,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,kBAAkB,CAQ9G"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The **Geographic Rule Engine** convention model (Direction E, #289 — see
7
+ * `docs/articles/plan/2026-06-05-geographic-rule-engine.md` and epic #288).
8
+ *
9
+ * A `Convention` is a declarative resolution profile attached to a Who's-On-First admin polygon.
10
+ * The engine deep-merges the conventions along a resolved place's ancestor chain — country →
11
+ * region → … → locality, most-specific winning — and the backend dispatches the named strategies
12
+ * in `candidateStrategies`, first to return candidates wins.
13
+ *
14
+ * This module is the backend-agnostic core: the convention TYPES, the deep-merge, and the seed
15
+ * source. The strategy IMPLEMENTATIONS are SQL-bound and live in `lookup.ts`, registered by
16
+ * name.
17
+ *
18
+ * For the existing EU locales (DE/FR/GB/NL) the seed source is empty, so every query resolves to
19
+ * `WORLD_DEFAULT` and the dispatch is byte-identical to the pre-engine coordinate-first path. JP
20
+ * / KR / TW add rows here (and #290 swaps the seed map for a build-from-source sqlite-backed
21
+ * source).
22
+ */
23
+ /**
24
+ * The base layer every ancestor chain starts from. Reproduces the pre-engine coordinate-first
25
+ * behavior exactly: try `postcode_area_resolution`, else fall back to fuzzy name match; soft-score
26
+ * weights 0.6 / 0.3 / 0.1. Changing these changes EU behavior — don't, without a byte-stability
27
+ * run.
28
+ */
29
+ export const WORLD_DEFAULT = {
30
+ candidateStrategies: ["postcode_area_resolution", "fallback_fuzzy_name_match"],
31
+ scoringWeights: { pc: 0.6, name: 0.3, pop: 0.1 },
32
+ };
33
+ /**
34
+ * The strategy names the backend registers. The single source of truth shared by the dispatch
35
+ * registry and the build-time validator, so an authored convention that names a non-existent
36
+ * strategy is caught at build (loud) rather than silently skipped at runtime.
37
+ */
38
+ export const BUILTIN_STRATEGY_NAMES = ["postcode_area_resolution", "fallback_fuzzy_name_match"];
39
+ /**
40
+ * Table name for the convention asset (#290). Carried here so the build script, the runtime source,
41
+ * and the shard auto-detect all agree.
42
+ */
43
+ export const ADDRESS_CONVENTION_TABLE = "address_convention";
44
+ /**
45
+ * In-memory convention source seeded from a `{ wofId: Convention }` map. Empty for the EU locales
46
+ * (they ride `WORLD_DEFAULT`); JP / KR / TW add rows. #290 replaces this with a sqlite-backed
47
+ * source built from source, same distributable-asset discipline as `postcode-locality-intl.db`.
48
+ */
49
+ export class SeedConventionSource {
50
+ #rows;
51
+ constructor(rows = {}) {
52
+ this.#rows = new Map(Object.entries(rows).map(([k, v]) => [Number(k), v]));
53
+ }
54
+ get(wofId) {
55
+ return this.#rows.get(wofId);
56
+ }
57
+ }
58
+ /**
59
+ * Deep-merge convention layers, later (more-specific) layers winning per field.
60
+ * `candidateStrategies` is replaced wholesale — a convention names its full ordered list, it does
61
+ * not append. `scoringWeights` is merged key-by-key so a locality can nudge one weight without
62
+ * restating the others.
63
+ */
64
+ export function mergeConventions(base, ...overrides) {
65
+ const out = {
66
+ candidateStrategies: base.candidateStrategies ? [...base.candidateStrategies] : undefined,
67
+ scoringWeights: base.scoringWeights ? { ...base.scoringWeights } : undefined,
68
+ };
69
+ for (const o of overrides) {
70
+ if (!o)
71
+ continue;
72
+ if (o.candidateStrategies !== undefined)
73
+ out.candidateStrategies = [...o.candidateStrategies];
74
+ if (o.scoringWeights !== undefined) {
75
+ out.scoringWeights = { ...(out.scoringWeights ?? WORLD_DEFAULT.scoringWeights), ...o.scoringWeights };
76
+ }
77
+ }
78
+ return out;
79
+ }
80
+ /**
81
+ * Resolve the effective convention for a place given its ancestor chain, ordered MOST-GENERAL →
82
+ * MOST-SPECIFIC (country, region, …, locality). Starts from `WORLD_DEFAULT` so every field is
83
+ * defined regardless of which (if any) ancestors carry an override.
84
+ */
85
+ export function resolveConvention(source, ancestorIds) {
86
+ const layers = ancestorIds.map((id) => source.get(id));
87
+ const merged = mergeConventions(WORLD_DEFAULT, ...layers);
88
+ return {
89
+ candidateStrategies: merged.candidateStrategies ?? WORLD_DEFAULT.candidateStrategies,
90
+ // Fill any weight gaps from the base so strategies always see a complete set.
91
+ scoringWeights: { ...WORLD_DEFAULT.scoringWeights, ...merged.scoringWeights },
92
+ };
93
+ }
94
+ //# sourceMappingURL=convention.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convention.js","sourceRoot":"","sources":["../convention.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAuCH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAuB;IAChD,mBAAmB,EAAE,CAAC,0BAA0B,EAAE,2BAA2B,CAAC;IAC9E,cAAc,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE;CAChD,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,0BAA0B,EAAE,2BAA2B,CAAU,CAAA;AAExG;;;GAGG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,oBAAoB,CAAA;AAgB5D;;;;GAIG;AACH,MAAM,OAAO,oBAAoB;IACvB,KAAK,CAAyB;IAEvC,YAAY,OAAmC,EAAE;QAChD,IAAI,CAAC,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IAC3E,CAAC;IAED,GAAG,CAAC,KAAa;QAChB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;IAC7B,CAAC;CACD;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAgB,EAAE,GAAG,SAAwC;IAC7F,MAAM,GAAG,GAAe;QACvB,mBAAmB,EAAE,IAAI,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,SAAS;QACzF,cAAc,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,SAAS;KAC5E,CAAA;IACD,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;QAC3B,IAAI,CAAC,CAAC;YAAE,SAAQ;QAChB,IAAI,CAAC,CAAC,mBAAmB,KAAK,SAAS;YAAE,GAAG,CAAC,mBAAmB,GAAG,CAAC,GAAG,CAAC,CAAC,mBAAmB,CAAC,CAAA;QAC7F,IAAI,CAAC,CAAC,cAAc,KAAK,SAAS,EAAE,CAAC;YACpC,GAAG,CAAC,cAAc,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,cAAc,IAAI,aAAa,CAAC,cAAc,CAAC,EAAE,GAAG,CAAC,CAAC,cAAc,EAAE,CAAA;QACtG,CAAC;IACF,CAAC;IACD,OAAO,GAAG,CAAA;AACX,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAwB,EAAE,WAA8B;IACzF,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;IACtD,MAAM,MAAM,GAAG,gBAAgB,CAAC,aAAa,EAAE,GAAG,MAAM,CAAC,CAAA;IACzD,OAAO;QACN,mBAAmB,EAAE,MAAM,CAAC,mBAAmB,IAAI,aAAa,CAAC,mBAAmB;QACpF,8EAA8E;QAC9E,cAAc,EAAE,EAAE,GAAG,aAAa,CAAC,cAAc,EAAE,GAAG,MAAM,CAAC,cAAc,EAAE;KAC7E,CAAA;AACF,CAAC"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * FST-based autocomplete. Prefix walk + BFS expansion to collect ranked place suggestions. O(depth
7
+ * × branching) — the FST IS the autocomplete index.
8
+ *
9
+ * Two query shapes are handled (the FST is a trie over normalized WORD tokens):
10
+ *
11
+ * - COMPLETE tokens ("new york") — `walk` lands on a state; collect its accepting entries + BFS a
12
+ * couple tokens past it for nearby completions. This is the CLI's "complete a place word"
13
+ * path.
14
+ * - A PARTIAL last token ("new yor", "chic") — `walk` fails (there is no "yor" edge, only "york"). So
15
+ * walk the complete prefix, then complete the partial token by prefix-filtering the
16
+ * continuation edges (`token.startsWith(partial)`). This is what a char-level typeahead
17
+ * needs; without it "new yor" returns nothing useful. (#587)
18
+ */
19
+ import { FstMatcher } from "./fst-matcher.js";
20
+ export interface AutocompleteResult {
21
+ query: string;
22
+ normalizedTokens: string[];
23
+ depth: number;
24
+ suggestions: AutocompleteSuggestion[];
25
+ }
26
+ export interface AutocompleteSuggestion {
27
+ name: string;
28
+ placetype: string;
29
+ importance: number;
30
+ wofID: number;
31
+ parentChain: number[];
32
+ matchDepth: number;
33
+ completionTokens: string[];
34
+ }
35
+ export interface AutocompleteOpts {
36
+ maxSuggestions?: number;
37
+ maxExpansionDepth?: number;
38
+ /**
39
+ * Collapse same-name suggestions to the single highest-importance one. Off by default (the CLI
40
+ * surfaces distinct same-name places — New York the city vs the county); a typeahead wants it ON
41
+ * so the dropdown isn't four "New London"s. (#587)
42
+ */
43
+ dedupeByName?: boolean;
44
+ }
45
+ /**
46
+ * Autocomplete from the current prefix. Returns suggestions ranked importance-descending.
47
+ */
48
+ export declare function autocomplete(fst: FstMatcher, query: string, opts?: AutocompleteOpts): AutocompleteResult;
49
+ //# sourceMappingURL=fst-autocomplete.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fst-autocomplete.d.ts","sourceRoot":"","sources":["../fst-autocomplete.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,UAAU,EAAmB,MAAM,kBAAkB,CAAA;AAG9D,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,gBAAgB,EAAE,MAAM,EAAE,CAAA;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,sBAAsB,EAAE,CAAA;CACrC;AAED,MAAM,WAAW,sBAAsB;IACtC,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB,EAAE,MAAM,EAAE,CAAA;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAChC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;CACtB;AAoBD;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,gBAAqB,GAAG,kBAAkB,CA2D5G"}