@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.
- package/lib/address/point/interpolation.ts +4 -4
- package/lib/address/point/spine-schema.ts +59 -0
- package/lib/address/point.ts +44 -1
- package/lib/ancestry/backfill.ts +3 -25
- package/lib/build/candidate.ts +22 -5
- package/lib/build/slim.ts +0 -19
- package/lib/candidate/alias-bags.ts +32 -14
- package/lib/candidate/bare-admin-names.ts +54 -0
- package/lib/candidate/importance.ts +2 -2
- package/lib/candidate/lookup.ts +63 -30
- package/lib/candidate/own-name.ts +1 -5
- package/lib/candidate/scoring.ts +4 -4
- package/lib/capitals.ts +2 -2
- package/lib/currency-backfill.ts +4 -4
- package/lib/fst/autocomplete.ts +1 -1
- package/lib/fst/binary-matcher.ts +379 -0
- package/lib/fst/builder.ts +1 -1
- package/lib/fst/deserialize-web.ts +14 -143
- package/lib/fst/matcher.ts +3 -3
- package/lib/fst/serialize.ts +1 -1
- package/lib/fst.ts +1 -0
- package/lib/geonames/aliases.ts +2 -2
- package/lib/index.ts +0 -1
- package/lib/interpolation.ts +3 -2
- package/lib/landmark/rows.ts +91 -0
- package/lib/landmark.ts +94 -0
- package/lib/lookup.ts +7 -27
- package/lib/nsul/lookup.ts +2 -1
- package/lib/nsul/schema.ts +2 -8
- package/lib/place-importance-schema.ts +1 -1
- package/lib/poi/lookup.ts +14 -14
- package/lib/primary-preference.ts +13 -20
- package/lib/proximity-rerank.ts +2 -2
- package/lib/reverse.ts +8 -5
- package/lib/schema.ts +0 -18
- package/lib/search-fetch.ts +3 -3
- package/lib/sqlite/convention-source.ts +1 -1
- package/lib/street/centroid.ts +2 -2
- package/lib/street/morphology/fst/builder.ts +3 -3
- package/lib/street/morphology/fst/loader.ts +3 -3
- package/lib/street/normalize.ts +150 -1
- package/lib/types.ts +15 -2
- package/lib/uprn/existence.ts +0 -1
- package/lib/uprn/lookup.ts +5 -5
- package/lib/uprn/schema.ts +3 -4
- package/lib/weights-overlay-linker.ts +4 -4
- package/out/address/point/interpolation.js +4 -4
- package/out/address/point/interpolation.js.map +1 -1
- package/out/address/point/spine-schema.d.ts +40 -0
- package/out/address/point/spine-schema.d.ts.map +1 -0
- package/out/address/point/spine-schema.js +40 -0
- package/out/address/point/spine-schema.js.map +1 -0
- package/out/address/point.d.ts.map +1 -1
- package/out/address/point.js +36 -1
- package/out/address/point.js.map +1 -1
- package/out/ancestry/backfill.d.ts.map +1 -1
- package/out/ancestry/backfill.js +3 -22
- package/out/ancestry/backfill.js.map +1 -1
- package/out/build/candidate.d.ts.map +1 -1
- package/out/build/candidate.js +17 -5
- package/out/build/candidate.js.map +1 -1
- package/out/build/slim.d.ts +1 -1
- package/out/build/slim.d.ts.map +1 -1
- package/out/build/slim.js +0 -12
- package/out/build/slim.js.map +1 -1
- package/out/candidate/alias-bags.d.ts +8 -0
- package/out/candidate/alias-bags.d.ts.map +1 -1
- package/out/candidate/alias-bags.js +20 -11
- package/out/candidate/alias-bags.js.map +1 -1
- package/out/candidate/bare-admin-names.d.ts +26 -0
- package/out/candidate/bare-admin-names.d.ts.map +1 -0
- package/out/candidate/bare-admin-names.js +38 -0
- package/out/candidate/bare-admin-names.js.map +1 -0
- package/out/candidate/importance.js +2 -2
- package/out/candidate/importance.js.map +1 -1
- package/out/candidate/lookup.d.ts.map +1 -1
- package/out/candidate/lookup.js +43 -23
- package/out/candidate/lookup.js.map +1 -1
- package/out/candidate/own-name.d.ts.map +1 -1
- package/out/candidate/own-name.js +1 -5
- package/out/candidate/own-name.js.map +1 -1
- package/out/candidate/scoring.js +4 -4
- package/out/candidate/scoring.js.map +1 -1
- package/out/capitals.js +2 -2
- package/out/capitals.js.map +1 -1
- package/out/currency-backfill.js +4 -4
- package/out/currency-backfill.js.map +1 -1
- package/out/env.d.ts +3 -0
- package/out/env.d.ts.map +1 -1
- package/out/fst/autocomplete.js +1 -1
- package/out/fst/autocomplete.js.map +1 -1
- package/out/fst/binary-matcher.d.ts +34 -0
- package/out/fst/binary-matcher.d.ts.map +1 -0
- package/out/fst/binary-matcher.js +294 -0
- package/out/fst/binary-matcher.js.map +1 -0
- package/out/fst/builder.js +1 -1
- package/out/fst/builder.js.map +1 -1
- package/out/fst/deserialize-web.d.ts +11 -6
- package/out/fst/deserialize-web.d.ts.map +1 -1
- package/out/fst/deserialize-web.js +12 -94
- package/out/fst/deserialize-web.js.map +1 -1
- package/out/fst/matcher.js +3 -3
- package/out/fst/matcher.js.map +1 -1
- package/out/fst/serialize.js +1 -1
- package/out/fst/serialize.js.map +1 -1
- package/out/fst.d.ts +1 -0
- package/out/fst.d.ts.map +1 -1
- package/out/fst.js +1 -0
- package/out/fst.js.map +1 -1
- package/out/geonames/aliases.js +2 -2
- package/out/geonames/aliases.js.map +1 -1
- package/out/index.d.ts +1 -1
- package/out/index.d.ts.map +1 -1
- package/out/index.js.map +1 -1
- package/out/interpolation.d.ts.map +1 -1
- package/out/interpolation.js +3 -2
- package/out/interpolation.js.map +1 -1
- package/out/landmark/rows.d.ts +63 -0
- package/out/landmark/rows.d.ts.map +1 -0
- package/out/landmark/rows.js +48 -0
- package/out/landmark/rows.js.map +1 -0
- package/out/landmark.d.ts +40 -0
- package/out/landmark.d.ts.map +1 -0
- package/out/landmark.js +78 -0
- package/out/landmark.js.map +1 -0
- package/out/lookup.d.ts +3 -7
- package/out/lookup.d.ts.map +1 -1
- package/out/lookup.js +4 -21
- package/out/lookup.js.map +1 -1
- package/out/nsul/lookup.d.ts +1 -1
- package/out/nsul/lookup.d.ts.map +1 -1
- package/out/nsul/lookup.js +1 -1
- package/out/nsul/lookup.js.map +1 -1
- package/out/nsul/schema.d.ts +2 -5
- package/out/nsul/schema.d.ts.map +1 -1
- package/out/nsul/schema.js +2 -7
- package/out/nsul/schema.js.map +1 -1
- package/out/place-importance-schema.js +1 -1
- package/out/place-importance-schema.js.map +1 -1
- package/out/poi/lookup.d.ts +3 -3
- package/out/poi/lookup.d.ts.map +1 -1
- package/out/poi/lookup.js +9 -9
- package/out/poi/lookup.js.map +1 -1
- package/out/primary-preference.d.ts +10 -10
- package/out/primary-preference.d.ts.map +1 -1
- package/out/primary-preference.js +8 -9
- package/out/primary-preference.js.map +1 -1
- package/out/proximity-rerank.js +2 -2
- package/out/proximity-rerank.js.map +1 -1
- package/out/reverse.d.ts.map +1 -1
- package/out/reverse.js +7 -5
- package/out/reverse.js.map +1 -1
- package/out/schema.d.ts +0 -17
- package/out/schema.d.ts.map +1 -1
- package/out/search-fetch.js +3 -3
- package/out/search-fetch.js.map +1 -1
- package/out/sqlite/convention-source.js +1 -1
- package/out/sqlite/convention-source.js.map +1 -1
- package/out/street/centroid.js +2 -2
- package/out/street/centroid.js.map +1 -1
- package/out/street/morphology/fst/builder.d.ts +2 -2
- package/out/street/morphology/fst/builder.js +2 -2
- package/out/street/morphology/fst/builder.js.map +1 -1
- package/out/street/morphology/fst/loader.d.ts +2 -2
- package/out/street/morphology/fst/loader.js +2 -2
- package/out/street/morphology/fst/loader.js.map +1 -1
- package/out/street/normalize.d.ts +23 -0
- package/out/street/normalize.d.ts.map +1 -1
- package/out/street/normalize.js +126 -1
- package/out/street/normalize.js.map +1 -1
- package/out/types.d.ts +13 -2
- package/out/types.d.ts.map +1 -1
- package/out/uprn/existence.d.ts.map +1 -1
- package/out/uprn/existence.js.map +1 -1
- package/out/uprn/lookup.d.ts.map +1 -1
- package/out/uprn/lookup.js +3 -3
- package/out/uprn/lookup.js.map +1 -1
- package/out/uprn/schema.d.ts +1 -1
- package/out/uprn/schema.d.ts.map +1 -1
- package/out/uprn/schema.js +1 -1
- package/out/uprn/schema.js.map +1 -1
- package/out/weights-overlay-linker.js +4 -4
- package/out/weights-overlay-linker.js.map +1 -1
- package/package.json +16 -13
- package/lib/coincident-roles.ts +0 -347
- package/out/coincident-roles.d.ts +0 -121
- package/out/coincident-roles.d.ts.map +0 -1
- package/out/coincident-roles.js +0 -222
- 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 {
|
|
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 =
|
|
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 =
|
|
216
|
-
const overshootM =
|
|
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
|
+
}
|
package/lib/address/point.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/lib/ancestry/backfill.ts
CHANGED
|
@@ -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/
|
|
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 =
|
|
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
|
|
package/lib/build/candidate.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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: {
|
|
65
|
-
|
|
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
|
|
101
|
+
const bare = opts.bareName?.(a.cid, piece.trim())
|
|
102
|
+
const keys = [normalizeLocalityForKey(piece), bare ? normalizeLocalityForKey(bare) : ""]
|
|
91
103
|
|
|
92
|
-
|
|
93
|
-
|
|
104
|
+
for (const [position, k] of keys.entries()) {
|
|
105
|
+
if (!k || seen.has(k)) continue
|
|
106
|
+
seen.add(k)
|
|
94
107
|
|
|
95
|
-
|
|
96
|
-
|
|
108
|
+
if (isRegion) {
|
|
109
|
+
const holder = official.get(`${a.cid}\0${k}`)
|
|
97
110
|
|
|
98
|
-
|
|
99
|
-
|
|
111
|
+
if (typeof holder === "number" && holder !== id) {
|
|
112
|
+
regionOfficialRefused++
|
|
100
113
|
|
|
101
|
-
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
102
116
|
}
|
|
103
|
-
}
|
|
104
117
|
|
|
105
|
-
|
|
118
|
+
stageRow(k, a, id, 0)
|
|
106
119
|
|
|
107
|
-
|
|
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 {
|
|
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 =
|
|
147
|
+
const km = haversine(lat, lon, place.lat, place.lon)
|
|
148
148
|
|
|
149
149
|
if (km < bestKm) {
|
|
150
150
|
bestKm = km
|