@mailwoman/resolver-wof-sqlite 10.7.0 → 11.0.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 (189) hide show
  1. package/lib/address/point/interpolation.ts +4 -4
  2. package/lib/address/point/spine-schema.ts +59 -0
  3. package/lib/address/point.ts +44 -1
  4. package/lib/ancestry/backfill.ts +3 -25
  5. package/lib/build/candidate.ts +22 -5
  6. package/lib/build/slim.ts +0 -19
  7. package/lib/candidate/alias-bags.ts +32 -14
  8. package/lib/candidate/bare-admin-names.ts +54 -0
  9. package/lib/candidate/importance.ts +2 -2
  10. package/lib/candidate/lookup.ts +63 -30
  11. package/lib/candidate/own-name.ts +1 -5
  12. package/lib/candidate/scoring.ts +4 -4
  13. package/lib/capitals.ts +2 -2
  14. package/lib/currency-backfill.ts +4 -4
  15. package/lib/fst/autocomplete.ts +1 -1
  16. package/lib/fst/binary-matcher.ts +379 -0
  17. package/lib/fst/builder.ts +1 -1
  18. package/lib/fst/deserialize-web.ts +14 -143
  19. package/lib/fst/matcher.ts +3 -3
  20. package/lib/fst/serialize.ts +1 -1
  21. package/lib/fst.ts +1 -0
  22. package/lib/geonames/aliases.ts +2 -2
  23. package/lib/index.ts +0 -1
  24. package/lib/interpolation.ts +3 -2
  25. package/lib/landmark/rows.ts +91 -0
  26. package/lib/landmark.ts +94 -0
  27. package/lib/lookup.ts +7 -27
  28. package/lib/nsul/lookup.ts +2 -1
  29. package/lib/nsul/schema.ts +2 -8
  30. package/lib/place-importance-schema.ts +1 -1
  31. package/lib/poi/lookup.ts +14 -14
  32. package/lib/primary-preference.ts +13 -20
  33. package/lib/proximity-rerank.ts +2 -2
  34. package/lib/reverse.ts +8 -5
  35. package/lib/schema.ts +0 -18
  36. package/lib/search-fetch.ts +3 -3
  37. package/lib/sqlite/convention-source.ts +1 -1
  38. package/lib/street/centroid.ts +2 -2
  39. package/lib/street/morphology/fst/builder.ts +3 -3
  40. package/lib/street/morphology/fst/loader.ts +3 -3
  41. package/lib/street/normalize.ts +150 -1
  42. package/lib/types.ts +15 -2
  43. package/lib/uprn/existence.ts +0 -1
  44. package/lib/uprn/lookup.ts +5 -5
  45. package/lib/uprn/schema.ts +3 -4
  46. package/lib/weights-overlay-linker.ts +4 -4
  47. package/out/address/point/interpolation.js +4 -4
  48. package/out/address/point/interpolation.js.map +1 -1
  49. package/out/address/point/spine-schema.d.ts +40 -0
  50. package/out/address/point/spine-schema.d.ts.map +1 -0
  51. package/out/address/point/spine-schema.js +40 -0
  52. package/out/address/point/spine-schema.js.map +1 -0
  53. package/out/address/point.d.ts.map +1 -1
  54. package/out/address/point.js +36 -1
  55. package/out/address/point.js.map +1 -1
  56. package/out/ancestry/backfill.d.ts.map +1 -1
  57. package/out/ancestry/backfill.js +3 -22
  58. package/out/ancestry/backfill.js.map +1 -1
  59. package/out/build/candidate.d.ts.map +1 -1
  60. package/out/build/candidate.js +17 -5
  61. package/out/build/candidate.js.map +1 -1
  62. package/out/build/slim.d.ts +1 -1
  63. package/out/build/slim.d.ts.map +1 -1
  64. package/out/build/slim.js +0 -12
  65. package/out/build/slim.js.map +1 -1
  66. package/out/candidate/alias-bags.d.ts +8 -0
  67. package/out/candidate/alias-bags.d.ts.map +1 -1
  68. package/out/candidate/alias-bags.js +20 -11
  69. package/out/candidate/alias-bags.js.map +1 -1
  70. package/out/candidate/bare-admin-names.d.ts +26 -0
  71. package/out/candidate/bare-admin-names.d.ts.map +1 -0
  72. package/out/candidate/bare-admin-names.js +38 -0
  73. package/out/candidate/bare-admin-names.js.map +1 -0
  74. package/out/candidate/importance.js +2 -2
  75. package/out/candidate/importance.js.map +1 -1
  76. package/out/candidate/lookup.d.ts.map +1 -1
  77. package/out/candidate/lookup.js +43 -23
  78. package/out/candidate/lookup.js.map +1 -1
  79. package/out/candidate/own-name.d.ts.map +1 -1
  80. package/out/candidate/own-name.js +1 -5
  81. package/out/candidate/own-name.js.map +1 -1
  82. package/out/candidate/scoring.js +4 -4
  83. package/out/candidate/scoring.js.map +1 -1
  84. package/out/capitals.js +2 -2
  85. package/out/capitals.js.map +1 -1
  86. package/out/currency-backfill.js +4 -4
  87. package/out/currency-backfill.js.map +1 -1
  88. package/out/env.d.ts +3 -0
  89. package/out/env.d.ts.map +1 -1
  90. package/out/fst/autocomplete.js +1 -1
  91. package/out/fst/autocomplete.js.map +1 -1
  92. package/out/fst/binary-matcher.d.ts +34 -0
  93. package/out/fst/binary-matcher.d.ts.map +1 -0
  94. package/out/fst/binary-matcher.js +294 -0
  95. package/out/fst/binary-matcher.js.map +1 -0
  96. package/out/fst/builder.js +1 -1
  97. package/out/fst/builder.js.map +1 -1
  98. package/out/fst/deserialize-web.d.ts +11 -6
  99. package/out/fst/deserialize-web.d.ts.map +1 -1
  100. package/out/fst/deserialize-web.js +12 -94
  101. package/out/fst/deserialize-web.js.map +1 -1
  102. package/out/fst/matcher.js +3 -3
  103. package/out/fst/matcher.js.map +1 -1
  104. package/out/fst/serialize.js +1 -1
  105. package/out/fst/serialize.js.map +1 -1
  106. package/out/fst.d.ts +1 -0
  107. package/out/fst.d.ts.map +1 -1
  108. package/out/fst.js +1 -0
  109. package/out/fst.js.map +1 -1
  110. package/out/geonames/aliases.js +2 -2
  111. package/out/geonames/aliases.js.map +1 -1
  112. package/out/index.d.ts +1 -1
  113. package/out/index.d.ts.map +1 -1
  114. package/out/index.js.map +1 -1
  115. package/out/interpolation.d.ts.map +1 -1
  116. package/out/interpolation.js +3 -2
  117. package/out/interpolation.js.map +1 -1
  118. package/out/landmark/rows.d.ts +63 -0
  119. package/out/landmark/rows.d.ts.map +1 -0
  120. package/out/landmark/rows.js +48 -0
  121. package/out/landmark/rows.js.map +1 -0
  122. package/out/landmark.d.ts +40 -0
  123. package/out/landmark.d.ts.map +1 -0
  124. package/out/landmark.js +78 -0
  125. package/out/landmark.js.map +1 -0
  126. package/out/lookup.d.ts +3 -7
  127. package/out/lookup.d.ts.map +1 -1
  128. package/out/lookup.js +4 -21
  129. package/out/lookup.js.map +1 -1
  130. package/out/nsul/lookup.d.ts +1 -1
  131. package/out/nsul/lookup.d.ts.map +1 -1
  132. package/out/nsul/lookup.js +1 -1
  133. package/out/nsul/lookup.js.map +1 -1
  134. package/out/nsul/schema.d.ts +2 -5
  135. package/out/nsul/schema.d.ts.map +1 -1
  136. package/out/nsul/schema.js +2 -7
  137. package/out/nsul/schema.js.map +1 -1
  138. package/out/place-importance-schema.js +1 -1
  139. package/out/place-importance-schema.js.map +1 -1
  140. package/out/poi/lookup.d.ts +3 -3
  141. package/out/poi/lookup.d.ts.map +1 -1
  142. package/out/poi/lookup.js +9 -9
  143. package/out/poi/lookup.js.map +1 -1
  144. package/out/primary-preference.d.ts +10 -10
  145. package/out/primary-preference.d.ts.map +1 -1
  146. package/out/primary-preference.js +8 -9
  147. package/out/primary-preference.js.map +1 -1
  148. package/out/proximity-rerank.js +2 -2
  149. package/out/proximity-rerank.js.map +1 -1
  150. package/out/reverse.d.ts.map +1 -1
  151. package/out/reverse.js +7 -5
  152. package/out/reverse.js.map +1 -1
  153. package/out/schema.d.ts +0 -17
  154. package/out/schema.d.ts.map +1 -1
  155. package/out/search-fetch.js +3 -3
  156. package/out/search-fetch.js.map +1 -1
  157. package/out/sqlite/convention-source.js +1 -1
  158. package/out/sqlite/convention-source.js.map +1 -1
  159. package/out/street/centroid.js +2 -2
  160. package/out/street/centroid.js.map +1 -1
  161. package/out/street/morphology/fst/builder.d.ts +2 -2
  162. package/out/street/morphology/fst/builder.js +2 -2
  163. package/out/street/morphology/fst/builder.js.map +1 -1
  164. package/out/street/morphology/fst/loader.d.ts +2 -2
  165. package/out/street/morphology/fst/loader.js +2 -2
  166. package/out/street/morphology/fst/loader.js.map +1 -1
  167. package/out/street/normalize.d.ts +23 -0
  168. package/out/street/normalize.d.ts.map +1 -1
  169. package/out/street/normalize.js +126 -1
  170. package/out/street/normalize.js.map +1 -1
  171. package/out/types.d.ts +13 -2
  172. package/out/types.d.ts.map +1 -1
  173. package/out/uprn/existence.d.ts.map +1 -1
  174. package/out/uprn/existence.js.map +1 -1
  175. package/out/uprn/lookup.d.ts.map +1 -1
  176. package/out/uprn/lookup.js +3 -3
  177. package/out/uprn/lookup.js.map +1 -1
  178. package/out/uprn/schema.d.ts +1 -1
  179. package/out/uprn/schema.d.ts.map +1 -1
  180. package/out/uprn/schema.js +1 -1
  181. package/out/uprn/schema.js.map +1 -1
  182. package/out/weights-overlay-linker.js +4 -4
  183. package/out/weights-overlay-linker.js.map +1 -1
  184. package/package.json +16 -13
  185. package/lib/coincident-roles.ts +0 -347
  186. package/out/coincident-roles.d.ts +0 -121
  187. package/out/coincident-roles.d.ts.map +0 -1
  188. package/out/coincident-roles.js +0 -222
  189. package/out/coincident-roles.js.map +0 -1
@@ -30,7 +30,7 @@
30
30
  */
31
31
 
32
32
  import type { InterpolationLookup } from "@mailwoman/core/resolver"
33
- import { haversineKm } from "@mailwoman/spatial"
33
+ import { haversine } from "@mailwoman/spatial/geometries/point/distance"
34
34
  import { DatabaseClient } from "@mailwoman/sqlite/client"
35
35
 
36
36
  import type { AddressPointDatabase } from "#address/point/schema"
@@ -185,7 +185,7 @@ function interpolateFromNeighbors(rows: readonly PointRow[], n: number): Interpo
185
185
 
186
186
  if (below && above) {
187
187
  const t = (n - below.n) / (above.n - below.n)
188
- const spanM = haversineKm(below.lat, below.lon, above.lat, above.lon) * 1000
188
+ const spanM = haversine(below.lat, below.lon, above.lat, above.lon) * 1000
189
189
 
190
190
  return {
191
191
  lat: below.lat + (above.lat - below.lat) * t,
@@ -212,8 +212,8 @@ function interpolateFromNeighbors(rows: readonly PointRow[], n: number): Interpo
212
212
 
213
213
  const lat = far.lat + (near.lat - far.lat) * t
214
214
  const lon = far.lon + (near.lon - far.lon) * t
215
- const pairM = haversineKm(near.lat, near.lon, far.lat, far.lon) * 1000
216
- const overshootM = haversineKm(lat, lon, near.lat, near.lon) * 1000
215
+ const pairM = haversine(near.lat, near.lon, far.lat, far.lon) * 1000
216
+ const overshootM = haversine(lat, lon, near.lat, near.lon) * 1000
217
217
 
218
218
  return {
219
219
  lat,
@@ -0,0 +1,59 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ * @file The shared rooftop schema with an H3 spine column and the layer-interface tables.
6
+ *
7
+ * The OpenStreetMap and G-NAF rooftop builders write this shape. Readers of existing address points
8
+ * project only the columns of `address_point` in `#address/point/schema`, so the H3 spine and the
9
+ * layer-interface tables leave their query path unchanged.
10
+ */
11
+
12
+ import { createLayerCoverageTable, createLayerManifestTable, type layerschemadatabase } from "@mailwoman/core/layers"
13
+ import type { Kysely } from "kysely"
14
+
15
+ import { ADDRESS_POINT_COLUMNS, type AddressPointTable, createAddressPointTable } from "#address/point/schema"
16
+
17
+ /**
18
+ * Jitter-stable address/POI join cell.
19
+ * Matches @mailwoman/address-id.
20
+ */
21
+ export const ADDRESS_POINT_H3_RESOLUTION = 9
22
+
23
+ export interface SpineAddressPointTable extends AddressPointTable {
24
+ /**
25
+ * 48-bit shortened H3 cell stored as a safe SQLite integer.
26
+ */
27
+ h3_cell: number
28
+ }
29
+
30
+ export interface SpineAddressPointDatabase extends layerschemadatabase {
31
+ address_point: SpineAddressPointTable
32
+ }
33
+
34
+ /**
35
+ * Positional insert order: the shared address-point fields followed by the H3 spine.
36
+ */
37
+ export const SPINE_ADDRESS_POINT_COLUMNS = [...ADDRESS_POINT_COLUMNS, "h3_cell"] as const
38
+
39
+ /**
40
+ * Create the legacy-compatible domain table plus the H3 spine and the layer-interface tables.
41
+ */
42
+ export async function createSpineAddressPointTables(db: Kysely<SpineAddressPointDatabase>): Promise<void> {
43
+ await createAddressPointTable(db)
44
+
45
+ await db.schema
46
+ .alterTable("address_point")
47
+ .addColumn("h3_cell", "integer", (column) => column.notNull())
48
+ .execute()
49
+
50
+ await createLayerManifestTable(db)
51
+ await createLayerCoverageTable(db)
52
+ }
53
+
54
+ /**
55
+ * Build after the bulk load so the spine index is not updated on every streaming insert.
56
+ */
57
+ export async function createSpineAddressPointIndexes(db: Kysely<SpineAddressPointDatabase>): Promise<void> {
58
+ await db.schema.createIndex("idx_ap_h3").on("address_point").column("h3_cell").execute()
59
+ }
@@ -7,6 +7,7 @@
7
7
 
8
8
  import type { AddressPointHit, AddressPointLookup } from "@mailwoman/core/resolver"
9
9
  import { DatabaseClient } from "@mailwoman/sqlite/client"
10
+ import { indexExists } from "@mailwoman/sqlite/introspection"
10
11
  import type { PathBuilderLike } from "path-ts"
11
12
 
12
13
  import type { AddressPointDatabase, AddressPointTable } from "#address/point/schema"
@@ -35,6 +36,27 @@ type AddressPointRow = Pick<AddressPointTable, "lat" | "lon" | "source" | "relea
35
36
  */
36
37
  const SELECT_COLS = "lat, lon, source, release, locality_norm, postcode"
37
38
 
39
+ /**
40
+ * The widest span of numbers a written range covers on one building or block.
41
+ */
42
+ const WIDEST_HOUSE_NUMBER_RANGE = 200
43
+
44
+ /**
45
+ * Returns whether two hyphenated numbers read as one number rather than a range of houses.
46
+ *
47
+ * `8-9 Upper St` is a range, and joining it would answer the different house at number 89.
48
+ * `19-4074` and Queens's `123-45` cannot be ranges, because a range climbs by a short step.
49
+ */
50
+ function isJoinedHyphenNumber(number: string): boolean {
51
+ const parts = /^(\d+)-(\d+)$/.exec(number)
52
+
53
+ if (!parts) return false
54
+
55
+ const span = Number(parts[2]) - Number(parts[1])
56
+
57
+ return span < 0 || span > WIDEST_HOUSE_NUMBER_RANGE
58
+ }
59
+
38
60
  /**
39
61
  * Finds an address point by exact normalized `(street, number)` within a postcode,
40
62
  * a locality, or a bounding box.
@@ -73,6 +95,7 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
73
95
  readonly #byLocalityTail:
74
96
  | PreparedGet<[street: StreetKey, number: string, tailPattern: string], AddressPointRow>
75
97
  | undefined
98
+ readonly #hasStreetIndex: boolean = false
76
99
  readonly #byBbox:
77
100
  | PreparedGet<
78
101
  [street: StreetKey, number: string, minLat: number, maxLat: number, minLon: number, maxLon: number],
@@ -140,6 +163,8 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
140
163
  WHERE locality_norm = ? AND street_norm = ? AND number = ? LIMIT 1`
141
164
  )
142
165
 
166
+ this.#hasStreetIndex = indexExists(this.#db, "idx_ap_street")
167
+
143
168
  this.#byBbox = prepareGet(
144
169
  this.#db,
145
170
  `SELECT ${SELECT_COLS} FROM address_point
@@ -220,6 +245,23 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
220
245
  ) {
221
246
  let row = this.#probe(streetNorm, number, query)
222
247
 
248
+ // A hyphenated number can be one number written two ways: Hawaii's `19-4074`
249
+ // is stored as `194074` by some sources.
250
+ // This runs ahead of the range fallback, because that fallback reads it as house number 19.
251
+ if (!row && isJoinedHyphenNumber(number)) {
252
+ row = this.#probe(streetNorm, number.replace("-", ""), query)
253
+ }
254
+
255
+ // A grid number leads with a compass letter.
256
+ // One writer's `N3971` is another source's `n 3971`.
257
+ if (!row) {
258
+ const grid = /^([nesw])(?:\s+)?(\d+)$/.exec(number)
259
+
260
+ if (grid) {
261
+ row = this.#probe(streetNorm, number.includes(" ") ? `${grid[1]}${grid[2]}` : `${grid[1]} ${grid[2]}`, query)
262
+ }
263
+ }
264
+
223
265
  // The registers store one number per point, so a range such as "385-387" falls back to its low end.
224
266
  if (!row) {
225
267
  const low = /^(\d+[a-z]?)-\d+[a-z]?$/.exec(number)?.[1]
@@ -307,7 +349,8 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
307
349
  }
308
350
 
309
351
  // The bounding box serves extracts whose points have no postcode or locality, such as OSM.
310
- if (!row && query.bbox) {
352
+ // A database without the street index skips this search, because each miss would scan the whole table.
353
+ if (!row && query.bbox && this.#hasStreetIndex) {
311
354
  const b = query.bbox
312
355
  const candidate = this.#byBbox!(streetNorm, number, b.minLat, b.maxLat, b.minLon, b.maxLon)
313
356
 
@@ -6,7 +6,8 @@
6
6
  * Repair the truncated ancestry left by the `parent_id` closure. Must run after `populateAncestors` and before the build freezes.
7
7
  */
8
8
 
9
- import { readWOFFeature } from "@mailwoman/core/resources/whosonfirst"
9
+ import { readWOFFeature } from "@mailwoman/gazetteer/whosonfirst"
10
+ import { hierarchyAncestorsInAnyBranch } from "@mailwoman/gazetteer/whosonfirst/hierarchy"
10
11
  import type { DatabaseClient } from "@mailwoman/sqlite/client"
11
12
  import { PathBuilder, type PathBuilderLike } from "path-ts"
12
13
  import { Globerator } from "spliterator/node/fs"
@@ -61,14 +62,6 @@ export async function discoverAdminDataRoots(reposRoot: PathBuilderLike): Promis
61
62
  return roots
62
63
  }
63
64
 
64
- // The `aid === id` check filters out a place's own `locality_id`.
65
- // For a neighborhood, that id identifies a real ancestor.
66
- function placetypeFromKey(key: string): string | null {
67
- if (!key.endsWith("_id")) return null
68
-
69
- return key.slice(0, -3)
70
- }
71
-
72
65
  /**
73
66
  * Insert missing ancestor rows for places whose ancestry chain ended before reaching a country.
74
67
  *
@@ -141,22 +134,7 @@ export async function backfillAncestorsFromHierarchy(
141
134
  continue
142
135
  }
143
136
 
144
- const seen = new Map<number, string>()
145
-
146
- for (const branch of hierarchy) {
147
- for (const [key, val] of Object.entries(branch)) {
148
- const pt = placetypeFromKey(key)
149
-
150
- if (!pt) continue
151
- const aid = Number(val)
152
-
153
- if (!Number.isFinite(aid) || aid <= 0 || aid === id) continue
154
-
155
- if (!seen.has(aid)) {
156
- seen.set(aid, pt)
157
- }
158
- }
159
- }
137
+ const seen = hierarchyAncestorsInAnyBranch(id, hierarchy)
160
138
 
161
139
  let present = alreadyPresent.get(id)
162
140
 
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { COUNTRY_POPULATION } from "@mailwoman/codex/country"
8
+ import { bareAdminName } from "@mailwoman/codex/country/bare-admin-names"
8
9
  import { pathExists } from "@mailwoman/core/fs/readers/stat"
9
10
  import { removePath } from "@mailwoman/core/fs/writers"
10
11
  import { stringifyJSON } from "@mailwoman/core/json"
@@ -14,6 +15,7 @@ import type { PathBuilderLike } from "path-ts"
14
15
  import { POPULATION_CORRECTIONS } from "#build/population-corrections"
15
16
  import { explodeAliasBags } from "#candidate/alias-bags"
16
17
  import { buildAncestorsSidecar } from "#candidate/ancestors/sidecar"
18
+ import { stageBareAdminNames } from "#candidate/bare-admin-names"
17
19
  import { stageCountryDisplayNames } from "#candidate/country-display-names"
18
20
  import { foldExtract } from "#candidate/extract-fold"
19
21
  import { createCandidateFTS } from "#candidate/fts"
@@ -205,6 +207,8 @@ export interface BuildCandidateResult {
205
207
  * Builds the candidate lookup database from a WOF admin database and replaces any existing output.
206
208
  * The builder adds an FTS5 trigram index for fuzzy lookup.
207
209
  */
210
+ // repo-health-ignore export-name-affix -- writes candidate.db, and the gazetteer
211
+ // pipeline's buildCandidate runs a whole build around it
208
212
  export async function buildCandidateTable(opts: BuildCandidateOptions): Promise<BuildCandidateResult> {
209
213
  const progress = opts.onProgress ?? (() => {})
210
214
 
@@ -226,7 +230,7 @@ export async function buildCandidateTable(opts: BuildCandidateOptions): Promise<
226
230
  const c = (code || "??").toUpperCase()
227
231
  let id = ccodes.get(c)
228
232
 
229
- if (id === undefined) {
233
+ if (typeof id !== "number") {
230
234
  id = ccodes.size
231
235
  ccodes.set(c, id)
232
236
  }
@@ -238,7 +242,7 @@ export async function buildCandidateTable(opts: BuildCandidateOptions): Promise<
238
242
  const p = pt || ""
239
243
  let id = ptcodes.get(p)
240
244
 
241
- if (id === undefined) {
245
+ if (typeof id !== "number") {
242
246
  id = ptcodes.size
243
247
  ptcodes.set(p, id)
244
248
  }
@@ -306,7 +310,7 @@ export async function buildCandidateTable(opts: BuildCandidateOptions): Promise<
306
310
  const ptid = ptID(r.placetype as string | null)
307
311
  const rid = regionOf.get(sid) ?? 0
308
312
 
309
- const wofPop = r.pop === null || r.pop === undefined ? null : Number(r.pop)
313
+ const wofPop = r.pop === null || !r.pop ? null : Number(r.pop)
310
314
 
311
315
  const pop =
312
316
  POPULATION_CORRECTIONS[sid]?.population ??
@@ -406,6 +410,16 @@ export async function buildCandidateTable(opts: BuildCandidateOptions): Promise<
406
410
  }).toLocaleString()} country surfaces`
407
411
  )
408
412
 
413
+ progress(
414
+ "bare-admin-names",
415
+ `${stageBareAdminNames({
416
+ attrs,
417
+ iso2ByID: new Map([...ccodes].map(([code, id]) => [id, code])),
418
+ stageRow,
419
+ tx: kdb,
420
+ }).toLocaleString()} bare admin names`
421
+ )
422
+
409
423
  if (opts.currencyBackfill) {
410
424
  const nBackfill = await resurrectCurrencyHoles({
411
425
  src,
@@ -429,14 +443,17 @@ export async function buildCandidateTable(opts: BuildCandidateOptions): Promise<
429
443
 
430
444
  progress("aliases", "exploding alias bags")
431
445
 
432
- const { nAlias, keyCounts, regionOfficialRefused } = explodeAliasBags(src, kdb, attrs, stageRow, {
446
+ const iso2ByCountryID = new Map([...ccodes].map(([code, id]) => [id, code]))
447
+
448
+ const { nAlias, nBare, keyCounts, regionOfficialRefused } = explodeAliasBags(src, kdb, attrs, stageRow, {
433
449
  regionPlacetypeID: ptID("region"),
434
450
  ccID,
451
+ bareName: (countryID, alias) => bareAdminName(iso2ByCountryID.get(countryID) ?? "", alias),
435
452
  })
436
453
 
437
454
  progress(
438
455
  "aliases",
439
- `${nAlias.toLocaleString()} aliases` +
456
+ `${nAlias.toLocaleString()} aliases; ${nBare.toLocaleString()} bare admin aliases` +
440
457
  (regionOfficialRefused
441
458
  ? `; ${regionOfficialRefused.toLocaleString()} region aliases refused as another region's official name`
442
459
  : "")
package/lib/build/slim.ts CHANGED
@@ -54,7 +54,6 @@ export type SlimBuildPhase =
54
54
  | "postcode"
55
55
  | "names"
56
56
  | "place_population"
57
- | "coincident_roles"
58
57
  | "place_abbr"
59
58
  | "fts"
60
59
  | "vacuum"
@@ -314,24 +313,6 @@ async function copyFromSource(
314
313
  .onConflict((oc) => oc.doNothing())
315
314
  .execute()
316
315
  }
317
-
318
- // `ancestors` is intentionally not copied, so the derived `coincident_roles`
319
- // table is copied and filtered to surviving spr ids.
320
- const relationSchema = out
321
- .prepare(`SELECT sql FROM src.sqlite_master WHERE type = 'table' AND name = 'coincident_roles'`)
322
- .get() as { sql?: string } | undefined
323
-
324
- if (relationSchema?.sql) {
325
- progress("coincident_roles", `${inputPath}: copying dual-role relation`)
326
- out.exec(relationSchema.sql.replace(/CREATE TABLE/i, "CREATE TABLE IF NOT EXISTS"))
327
-
328
- out.exec(
329
- `INSERT OR IGNORE INTO coincident_roles SELECT * FROM src.coincident_roles
330
- WHERE admin_id IN (SELECT id FROM spr) AND locality_id IN (SELECT id FROM spr)`
331
- )
332
-
333
- out.exec(`CREATE INDEX IF NOT EXISTS coincident_roles_by_admin ON coincident_roles (admin_id)`)
334
- }
335
316
  } finally {
336
317
  out.exec(`DETACH DATABASE src;`)
337
318
  }
@@ -61,9 +61,20 @@ export function explodeAliasBags(
61
61
  out: DatabaseClient<CandidateDatabase>,
62
62
  attrs: Map<number, PlaceAttrs>,
63
63
  stageRow: StageRow,
64
- opts: { regionPlacetypeID?: number; ccID?: (code: string | null) => number } = {}
65
- ): { nAlias: number; keyCounts: Map<number, number>; regionOfficialRefused: number } {
64
+ opts: {
65
+ regionPlacetypeID?: number
66
+ ccID?: (code: string | null) => number
67
+ /**
68
+ * The bare form of an alias in its place's country, staged as one more alias: `渋谷` for the alias `渋谷区`.
69
+ *
70
+ * A Japanese place's primary name is romanized, so its suffixed name reaches
71
+ * the candidate only as an alias.
72
+ */
73
+ bareName?: (countryID: number, alias: string) => string | null
74
+ } = {}
75
+ ): { nAlias: number; nBare: number; keyCounts: Map<number, number>; regionOfficialRefused: number } {
66
76
  let nAlias = 0
77
+ let nBare = 0
67
78
  let regionOfficialRefused = 0
68
79
  const keyCounts = new Map<number, number>()
69
80
 
@@ -87,24 +98,31 @@ export function explodeAliasBags(
87
98
  // includes surrounding whitespace and the last one is empty.
88
99
  // `normalizeLocalityForKey` folds both away.
89
100
  for (const piece of alt.split(ALIAS_SEPARATOR)) {
90
- const k = normalizeLocalityForKey(piece)
101
+ const bare = opts.bareName?.(a.cid, piece.trim())
102
+ const keys = [normalizeLocalityForKey(piece), bare ? normalizeLocalityForKey(bare) : ""]
91
103
 
92
- if (!k || seen.has(k)) continue
93
- seen.add(k)
104
+ for (const [position, k] of keys.entries()) {
105
+ if (!k || seen.has(k)) continue
106
+ seen.add(k)
94
107
 
95
- if (isRegion) {
96
- const holder = official.get(`${a.cid}\0${k}`)
108
+ if (isRegion) {
109
+ const holder = official.get(`${a.cid}\0${k}`)
97
110
 
98
- if (typeof holder === "number" && holder !== id) {
99
- regionOfficialRefused++
111
+ if (typeof holder === "number" && holder !== id) {
112
+ regionOfficialRefused++
100
113
 
101
- continue
114
+ continue
115
+ }
102
116
  }
103
- }
104
117
 
105
- stageRow(k, a, id, 0)
118
+ stageRow(k, a, id, 0)
106
119
 
107
- nAlias++
120
+ if (position) {
121
+ nBare++
122
+ } else {
123
+ nAlias++
124
+ }
125
+ }
108
126
  }
109
127
 
110
128
  keyCounts.set(id, seen.size)
@@ -112,5 +130,5 @@ export function explodeAliasBags(
112
130
 
113
131
  out.exec("COMMIT")
114
132
 
115
- return { nAlias, keyCounts, regionOfficialRefused }
133
+ return { nAlias, nBare, keyCounts, regionOfficialRefused }
116
134
  }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ * @file A candidate build pass that keys each admin place under its bare name as well as its full one.
6
+ */
7
+
8
+ import { bareAdminName } from "@mailwoman/codex/country/bare-admin-names"
9
+
10
+ import type { PlaceAttrs, StageRow } from "#candidate/place-attrs"
11
+ import { normalizeLocalityForKey } from "#street/normalize"
12
+
13
+ /**
14
+ * Stages an alias row under the bare form of every place name a country writes with an admin-unit suffix.
15
+ *
16
+ * A bare `渋谷` otherwise reaches only the places whose full name is `渋谷`, a Hyogo
17
+ * and a Chiba locality with no recorded population, and never Tokyo's `渋谷区`.
18
+ * The alias row includes the ward's own population, so the candidate ordering
19
+ * ranks it above the unmeasured exact matches.
20
+ *
21
+ * @returns The number of alias rows staged.
22
+ */
23
+ export function stageBareAdminNames(ctx: {
24
+ attrs: Map<number, PlaceAttrs>
25
+ iso2ByID: Map<number, string>
26
+ stageRow: StageRow
27
+ tx: { exec(sql: string): void }
28
+ }): number {
29
+ let staged = 0
30
+
31
+ ctx.tx.exec("BEGIN")
32
+
33
+ for (const [sid, a] of ctx.attrs) {
34
+ const iso2 = ctx.iso2ByID.get(a.cid)
35
+
36
+ if (!iso2) continue
37
+
38
+ const bare = bareAdminName(iso2, a.name)
39
+
40
+ if (!bare) continue
41
+
42
+ const key = normalizeLocalityForKey(bare)
43
+
44
+ if (!key || key === a.pkey) continue
45
+
46
+ ctx.stageRow(key, a, sid, 0)
47
+
48
+ staged++
49
+ }
50
+
51
+ ctx.tx.exec("COMMIT")
52
+
53
+ return staged
54
+ }
@@ -29,7 +29,7 @@
29
29
  * population assigned it.
30
30
  */
31
31
 
32
- import { haversineKm } from "@mailwoman/spatial"
32
+ import { haversine } from "@mailwoman/spatial/geometries/point/distance"
33
33
  import { DatabaseClient } from "@mailwoman/sqlite/client"
34
34
  import type { PathBuilderLike } from "path-ts"
35
35
 
@@ -144,7 +144,7 @@ export class ImportanceIndex {
144
144
  let bestKm = Infinity
145
145
 
146
146
  for (const place of group) {
147
- const km = haversineKm(lat, lon, place.lat, place.lon)
147
+ const km = haversine(lat, lon, place.lat, place.lon)
148
148
 
149
149
  if (km < bestKm) {
150
150
  bestKm = km