@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
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The landmark table's read, shared by the native lookup and the range-read lookup a Worker uses. It holds no database
|
|
7
|
+
* client, so a Worker bundle can import it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { LandmarkHit, LandmarkKind } from "@mailwoman/core/resolver"
|
|
11
|
+
|
|
12
|
+
import { normalizeLocalityForKey } from "#street/normalize"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One way of writing one place.
|
|
16
|
+
*/
|
|
17
|
+
export interface LandmarkNameTable {
|
|
18
|
+
name_key: string
|
|
19
|
+
qid: string
|
|
20
|
+
/**
|
|
21
|
+
* The place's own display name.
|
|
22
|
+
*/
|
|
23
|
+
name: string
|
|
24
|
+
kind: LandmarkKind
|
|
25
|
+
country: string | null
|
|
26
|
+
latitude: number
|
|
27
|
+
longitude: number
|
|
28
|
+
sitelinks: number
|
|
29
|
+
/**
|
|
30
|
+
* The upper-case spelling of a code alias such as `DTW`, or `null` for an ordinary name.
|
|
31
|
+
*
|
|
32
|
+
* A code answers only when written in that case, so `Lax` and `Sea` stay words.
|
|
33
|
+
*/
|
|
34
|
+
code: string | null
|
|
35
|
+
/**
|
|
36
|
+
* 1 when this is the place's own name in English or its country's language, 0 for a translation or alias.
|
|
37
|
+
*/
|
|
38
|
+
is_primary: number
|
|
39
|
+
/**
|
|
40
|
+
* The importance Photon's index holds for an OpenStreetMap place, or `null` for a Wikidata place.
|
|
41
|
+
*/
|
|
42
|
+
importance: number | null
|
|
43
|
+
/**
|
|
44
|
+
* The place's short H3 cell, the spatial key an OpenStreetMap layer's manifest names,
|
|
45
|
+
* or `null` for a Wikidata place.
|
|
46
|
+
*/
|
|
47
|
+
h3_cell: number | null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The read of every row under one name key, best first.
|
|
52
|
+
*/
|
|
53
|
+
export const LANDMARK_BY_NAME_SQL =
|
|
54
|
+
"SELECT * FROM landmark_name WHERE name_key = ? ORDER BY is_primary DESC, sitelinks DESC, importance DESC, qid"
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Returns the key a written name is stored and read under.
|
|
58
|
+
*
|
|
59
|
+
* Canonical Unicode form, a typographic apostrophe and a sentence's closing period do not change
|
|
60
|
+
* which place a name means, so `St George’s.` and `St George's` read one row.
|
|
61
|
+
*/
|
|
62
|
+
export function landmarkNameKey(written: string): string {
|
|
63
|
+
return normalizeLocalityForKey(written.normalize("NFC").replaceAll("’", "'").trim().replace(/\.+$/u, ""))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Turns the rows under a name's key into hits for that name as it was written.
|
|
68
|
+
*
|
|
69
|
+
* A code written in its own case is an identifier, so it ranks ahead of every word spelled the same.
|
|
70
|
+
* A code row reads only in that case.
|
|
71
|
+
*/
|
|
72
|
+
export function landmarkHitsFromRows(rows: readonly LandmarkNameTable[], name: string): LandmarkHit[] {
|
|
73
|
+
const written = name.trim()
|
|
74
|
+
const isCode = (row: LandmarkNameTable) => row.code === written
|
|
75
|
+
|
|
76
|
+
return rows
|
|
77
|
+
.filter((row) => !row.code || isCode(row))
|
|
78
|
+
.toSorted((a, b) => Number(isCode(b)) - Number(isCode(a)))
|
|
79
|
+
.map((row) => ({
|
|
80
|
+
code: isCode(row),
|
|
81
|
+
qid: row.qid,
|
|
82
|
+
name: row.name,
|
|
83
|
+
kind: row.kind,
|
|
84
|
+
country: row.country,
|
|
85
|
+
lat: row.latitude,
|
|
86
|
+
lon: row.longitude,
|
|
87
|
+
sitelinks: row.sitelinks,
|
|
88
|
+
primary: row.is_primary === 1,
|
|
89
|
+
importance: row.importance ?? null,
|
|
90
|
+
}))
|
|
91
|
+
}
|
package/lib/landmark.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The landmark layer: Wikidata places keyed by every name and alias they are written with, ranked by the number of
|
|
7
|
+
* Wikipedia language editions that describe them. The source is CC0, so the database may ship.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { LandmarkHit, LandmarkLookup } from "@mailwoman/core/resolver"
|
|
11
|
+
import { DatabaseClient } from "@mailwoman/sqlite/client"
|
|
12
|
+
import { sql, type Kysely } from "kysely"
|
|
13
|
+
import type { PathBuilderLike } from "path-ts"
|
|
14
|
+
|
|
15
|
+
import { LANDMARK_BY_NAME_SQL, landmarkHitsFromRows, landmarkNameKey, type LandmarkNameTable } from "#landmark/rows"
|
|
16
|
+
import { prepareAll, type PreparedAll } from "#sqlite-utils"
|
|
17
|
+
|
|
18
|
+
export interface LandmarkDatabase {
|
|
19
|
+
landmark_name: LandmarkNameTable
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The column order the build's positional insert writes.
|
|
24
|
+
*/
|
|
25
|
+
export const LANDMARK_NAME_COLUMNS = [
|
|
26
|
+
"name_key",
|
|
27
|
+
"qid",
|
|
28
|
+
"name",
|
|
29
|
+
"kind",
|
|
30
|
+
"country",
|
|
31
|
+
"latitude",
|
|
32
|
+
"longitude",
|
|
33
|
+
"sitelinks",
|
|
34
|
+
"code",
|
|
35
|
+
"is_primary",
|
|
36
|
+
"importance",
|
|
37
|
+
"h3_cell",
|
|
38
|
+
] as const satisfies ReadonlyArray<keyof LandmarkNameTable>
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates the table.
|
|
42
|
+
*
|
|
43
|
+
* Rows cluster by name, because every read is a probe of one name.
|
|
44
|
+
*/
|
|
45
|
+
export async function createLandmarkTables(db: Kysely<LandmarkDatabase>): Promise<void> {
|
|
46
|
+
await db.schema
|
|
47
|
+
.createTable("landmark_name")
|
|
48
|
+
.addColumn("name_key", "text", (c) => c.notNull())
|
|
49
|
+
.addColumn("qid", "text", (c) => c.notNull())
|
|
50
|
+
.addColumn("name", "text", (c) => c.notNull())
|
|
51
|
+
.addColumn("kind", "text", (c) => c.notNull())
|
|
52
|
+
.addColumn("country", "text")
|
|
53
|
+
.addColumn("latitude", "real", (c) => c.notNull())
|
|
54
|
+
.addColumn("longitude", "real", (c) => c.notNull())
|
|
55
|
+
.addColumn("sitelinks", "integer", (c) => c.notNull())
|
|
56
|
+
.addColumn("code", "text")
|
|
57
|
+
.addColumn("is_primary", "integer", (c) => c.notNull())
|
|
58
|
+
.addColumn("importance", "real")
|
|
59
|
+
.addColumn("h3_cell", "integer")
|
|
60
|
+
.addPrimaryKeyConstraint("landmark_name_pk", ["name_key", "qid"])
|
|
61
|
+
.modifyEnd(sql`without rowid`)
|
|
62
|
+
.execute()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Returns whether an alias is an identifier code rather than a word:
|
|
67
|
+
* two to five upper-case letters or digits.
|
|
68
|
+
*/
|
|
69
|
+
export function isLandmarkCode(alias: string): boolean {
|
|
70
|
+
return /^[\p{Lu}\d]{2,5}$/u.test(alias)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Reads a sealed landmark database.
|
|
75
|
+
*/
|
|
76
|
+
export class LandmarkSqliteLookup implements LandmarkLookup, Disposable {
|
|
77
|
+
readonly #db: DatabaseClient<LandmarkDatabase>
|
|
78
|
+
|
|
79
|
+
readonly #byName: PreparedAll<[nameKey: string], LandmarkNameTable>
|
|
80
|
+
|
|
81
|
+
constructor(path: PathBuilderLike) {
|
|
82
|
+
this.#db = new DatabaseClient<LandmarkDatabase>(path, { readOnly: true })
|
|
83
|
+
|
|
84
|
+
this.#byName = prepareAll(this.#db, LANDMARK_BY_NAME_SQL)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
find(name: string): LandmarkHit[] {
|
|
88
|
+
return landmarkHitsFromRows(this.#byName(landmarkNameKey(name)), name)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
[Symbol.dispose](): void {
|
|
92
|
+
this.#db[Symbol.dispose]()
|
|
93
|
+
}
|
|
94
|
+
}
|
package/lib/lookup.ts
CHANGED
|
@@ -7,16 +7,15 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { expandPlacetypeFilter } from "@mailwoman/codex/placetype-map"
|
|
10
|
-
import type { Ancestor
|
|
10
|
+
import type { Ancestor } from "@mailwoman/core/resolver"
|
|
11
11
|
import { allRows } from "@mailwoman/core/utils"
|
|
12
|
-
import {
|
|
12
|
+
import { haversine } from "@mailwoman/spatial/geometries/point/distance"
|
|
13
13
|
import type { DatabaseClient, SQLInputValue } from "@mailwoman/sqlite/client"
|
|
14
14
|
import { SQLiteLookup } from "@mailwoman/sqlite/lookup"
|
|
15
15
|
import type { PathBuilderLike } from "path-ts"
|
|
16
16
|
|
|
17
17
|
import { ancestorLineage } from "#ancestry"
|
|
18
18
|
import { candidateFromSearchRow, rankCandidates } from "#candidate/scoring"
|
|
19
|
-
import { loadCoincidentLocalities } from "#coincident-roles"
|
|
20
19
|
import {
|
|
21
20
|
ADDRESS_CONVENTION_TABLE,
|
|
22
21
|
resolveConvention,
|
|
@@ -81,7 +80,7 @@ export interface WOFSQLitePlaceLookupOpts {
|
|
|
81
80
|
*
|
|
82
81
|
* Absent, every alias code path is skipped and the resolver is byte-identical.
|
|
83
82
|
*/
|
|
84
|
-
postalCityAliases?: WOFPostalCityAliasLookup
|
|
83
|
+
postalCityAliases?: WOFPostalCityAliasLookup | null
|
|
85
84
|
}
|
|
86
85
|
|
|
87
86
|
/**
|
|
@@ -155,10 +154,6 @@ export class WOFSQLitePlaceLookup extends SQLiteLookup<WOFDatabase> implements P
|
|
|
155
154
|
* Strategy names already warned about, so an unknown name surfaces once.
|
|
156
155
|
*/
|
|
157
156
|
readonly #warnedUnknownStrategies = new Set<string>()
|
|
158
|
-
/**
|
|
159
|
-
* Lazily-built `admin_id → coincident localities` map, null until first use.
|
|
160
|
-
*/
|
|
161
|
-
#coincidentRolesCache: Map<number, CoincidentLocality[]> | null = null
|
|
162
157
|
/**
|
|
163
158
|
* Per-id memoized ancestor lineages, queried once per chain.
|
|
164
159
|
*/
|
|
@@ -168,7 +163,7 @@ export class WOFSQLitePlaceLookup extends SQLiteLookup<WOFDatabase> implements P
|
|
|
168
163
|
*/
|
|
169
164
|
readonly #postalCityAliases: WOFPostalCityAliasLookup | null
|
|
170
165
|
|
|
171
|
-
constructor(opts: WOFSQLitePlaceLookupOpts, weights?: Partial<RankingWeights>) {
|
|
166
|
+
constructor(opts: WOFSQLitePlaceLookupOpts, weights?: Partial<RankingWeights> | null) {
|
|
172
167
|
if (opts.database && opts.databasePath) {
|
|
173
168
|
throw new Error("WOFSQLitePlaceLookup: pass either `database` or `databasePath`, not both")
|
|
174
169
|
}
|
|
@@ -355,21 +350,6 @@ export class WOFSQLitePlaceLookup extends SQLiteLookup<WOFDatabase> implements P
|
|
|
355
350
|
return found
|
|
356
351
|
}
|
|
357
352
|
|
|
358
|
-
/**
|
|
359
|
-
* Dual-role localities coincident with an admin id, or `[]` when the relation table is absent.
|
|
360
|
-
*/
|
|
361
|
-
coincidentLocalitiesFor(adminID: number | string): CoincidentLocality[] {
|
|
362
|
-
const id = typeof adminID === "number" ? adminID : Number(adminID)
|
|
363
|
-
|
|
364
|
-
if (!Number.isFinite(id)) return []
|
|
365
|
-
|
|
366
|
-
if (!this.#coincidentRolesCache) {
|
|
367
|
-
this.#coincidentRolesCache = loadCoincidentLocalities(this.database)
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
return this.#coincidentRolesCache.get(id) ?? []
|
|
371
|
-
}
|
|
372
|
-
|
|
373
353
|
/**
|
|
374
354
|
* The ancestor lineage of a place, ordered nearest-first with self excluded,
|
|
375
355
|
* or `[]` when it has no recorded ancestry.
|
|
@@ -549,7 +529,7 @@ export class WOFSQLitePlaceLookup extends SQLiteLookup<WOFDatabase> implements P
|
|
|
549
529
|
#countryWOFId(code: string): number | null {
|
|
550
530
|
const cached = this.#countryWOFIdCache.get(code)
|
|
551
531
|
|
|
552
|
-
if (cached
|
|
532
|
+
if (cached) return cached
|
|
553
533
|
let id: number | null
|
|
554
534
|
|
|
555
535
|
try {
|
|
@@ -666,12 +646,12 @@ export class WOFSQLitePlaceLookup extends SQLiteLookup<WOFDatabase> implements P
|
|
|
666
646
|
// oxlint-disable-next-line unicorn/no-array-sort -- sorts a freshly-built array. toSorted would double-allocate on a hot path
|
|
667
647
|
.sort((a, b) => b.containing - a.containing || a.dist - b.dist)[0]
|
|
668
648
|
|
|
669
|
-
const anchor = anchorRow ? merged.get(anchorRow.id) :
|
|
649
|
+
const anchor = anchorRow ? merged.get(anchorRow.id) : null
|
|
670
650
|
|
|
671
651
|
if (
|
|
672
652
|
anchor &&
|
|
673
653
|
(top.id as number) !== anchorRow!.id &&
|
|
674
|
-
|
|
654
|
+
haversine(top.lat, top.lon, anchor.lat, anchor.lon) > CF_MISMATCH_KM
|
|
675
655
|
) {
|
|
676
656
|
top.mismatch = true
|
|
677
657
|
}
|
package/lib/nsul/lookup.ts
CHANGED
|
@@ -25,9 +25,10 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import type { UkCountryCode } from "@mailwoman/codex/gb/country"
|
|
28
|
+
import { compactPostcode } from "@mailwoman/postcode/display-form"
|
|
28
29
|
import { SQLiteLookup, type SQLiteLookupOptions } from "@mailwoman/sqlite/lookup"
|
|
29
30
|
|
|
30
|
-
import {
|
|
31
|
+
import type { NSULDatabase } from "#nsul/schema"
|
|
31
32
|
import { prepareAll, prepareGet } from "#sqlite-utils"
|
|
32
33
|
|
|
33
34
|
/**
|
package/lib/nsul/schema.ts
CHANGED
|
@@ -70,13 +70,6 @@ export interface NSULDatabase extends layerschemadatabase {
|
|
|
70
70
|
nsul_meta: NSULMetaTable
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
/**
|
|
74
|
-
* The compact form of a unit postcode, every space removed and upper-cased.
|
|
75
|
-
*/
|
|
76
|
-
export function compactPostcode(pcds: string): string {
|
|
77
|
-
return pcds.replaceAll(/\s+/g, "").toUpperCase()
|
|
78
|
-
}
|
|
79
|
-
|
|
80
73
|
export async function createUPRNPostcodeTable(db: Kysely<NSULDatabase>): Promise<void> {
|
|
81
74
|
await db.schema
|
|
82
75
|
.createTable("uprn_postcode")
|
|
@@ -102,7 +95,8 @@ export async function createNSULMetaTable(db: Kysely<NSULDatabase>): Promise<voi
|
|
|
102
95
|
/**
|
|
103
96
|
* The `pcds_compact` index the `uprnsForPostcode` probe reads.
|
|
104
97
|
*
|
|
105
|
-
* There is no index on the spaced `pcds` because
|
|
98
|
+
* There is no index on the spaced `pcds` because `compactPostcode` from
|
|
99
|
+
* `@mailwoman/postcode/display-form` derives the compact column from it.
|
|
106
100
|
*/
|
|
107
101
|
export async function createNSULIndexes(db: Kysely<NSULDatabase>): Promise<void> {
|
|
108
102
|
await db.schema.createIndex("uprn_postcode_pcds_compact").on("uprn_postcode").column("pcds_compact").execute()
|
|
@@ -262,7 +262,7 @@ export function loadImportanceSplit<DB>(db: DatabaseClient<DB>): ImportanceSplit
|
|
|
262
262
|
for (const row of rows) {
|
|
263
263
|
const split = splitLegacyImportance(row.importance, population.get(row.id))
|
|
264
264
|
|
|
265
|
-
if (split.encyclopedic
|
|
265
|
+
if (!split.encyclopedic) {
|
|
266
266
|
legacyFallbackRows++
|
|
267
267
|
} else {
|
|
268
268
|
encyclopedic.set(row.id, split.encyclopedic)
|
package/lib/poi/lookup.ts
CHANGED
|
@@ -7,16 +7,16 @@
|
|
|
7
7
|
* `without rowid` B-tree `poi-schema.ts` builds. Category, brand and name searches share one
|
|
8
8
|
* artifact. Each path has a method that returns its details.
|
|
9
9
|
*
|
|
10
|
-
* `latLngToCell` and `gridDisk` come from
|
|
10
|
+
* `latLngToCell` and `gridDisk` come from `@sister.software/h3`. The 48-bit short-cell packing that turns a raw
|
|
11
11
|
* H3 cell into the integer `poi.h3_cell` stores is `@mailwoman/spatial`'s `shortCellToInt`, and
|
|
12
12
|
* that math is never reimplemented here.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { allRows } from "@mailwoman/core/utils"
|
|
16
|
-
import {
|
|
16
|
+
import { haversine, shortCellToInt } from "@mailwoman/spatial"
|
|
17
17
|
import type { DatabaseClient } from "@mailwoman/sqlite/client"
|
|
18
18
|
import { SQLiteLookup, type SQLiteLookupOptions } from "@mailwoman/sqlite/lookup"
|
|
19
|
-
import { gridDisk, latLngToCell } from "h3
|
|
19
|
+
import { gridDisk, latLngToCell } from "@sister.software/h3"
|
|
20
20
|
|
|
21
21
|
import type { POICategoryCodeTable, POIDatabase, POITable } from "#poi/schema"
|
|
22
22
|
/**
|
|
@@ -79,7 +79,7 @@ export interface POISearchQuery {
|
|
|
79
79
|
*
|
|
80
80
|
* Required for category/brand queries (k-ring expansion).
|
|
81
81
|
*/
|
|
82
|
-
center?: { latitude: number; longitude: number }
|
|
82
|
+
center?: { latitude: number; longitude: number } | null
|
|
83
83
|
/**
|
|
84
84
|
* Ring budget: the count of res-9 k-rings to expand before giving up.
|
|
85
85
|
*
|
|
@@ -88,7 +88,7 @@ export interface POISearchQuery {
|
|
|
88
88
|
* @defaultValue `16` ≈ ~5.4 km
|
|
89
89
|
*/
|
|
90
90
|
maxRings?: number
|
|
91
|
-
limit?: number
|
|
91
|
+
limit?: number | null
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
export interface POISearchHit {
|
|
@@ -183,7 +183,7 @@ export class POILookup<DB extends POIDatabase = POIDatabase> extends SQLiteLooku
|
|
|
183
183
|
const limit = Math.max(1, query.limit ?? DEFAULT_LIMIT)
|
|
184
184
|
|
|
185
185
|
if (query.name) {
|
|
186
|
-
return this.#searchByName(query.name, limit, query.center)
|
|
186
|
+
return this.#searchByName(query.name, limit, query.center ?? undefined)
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
if (query.categoryIDs?.length || query.brandWikidata) {
|
|
@@ -216,7 +216,7 @@ export class POILookup<DB extends POIDatabase = POIDatabase> extends SQLiteLooku
|
|
|
216
216
|
|
|
217
217
|
return sortByDistance(rows, center)
|
|
218
218
|
.filter(
|
|
219
|
-
(row) =>
|
|
219
|
+
(row) => haversine(center.latitude, center.longitude, row.latitude, row.longitude) <= BRAND_MAX_DISTANCE_KM
|
|
220
220
|
)
|
|
221
221
|
.slice(0, limit)
|
|
222
222
|
.map((row) => toHit(row, this.#idToCategory, center))
|
|
@@ -235,7 +235,7 @@ export class POILookup<DB extends POIDatabase = POIDatabase> extends SQLiteLooku
|
|
|
235
235
|
for (const id of query.categoryIDs ?? []) {
|
|
236
236
|
const resolved = this.#categoryToID.get(id)
|
|
237
237
|
|
|
238
|
-
if (resolved
|
|
238
|
+
if (resolved) {
|
|
239
239
|
categoryIDs.push(resolved)
|
|
240
240
|
}
|
|
241
241
|
}
|
|
@@ -244,7 +244,7 @@ export class POILookup<DB extends POIDatabase = POIDatabase> extends SQLiteLooku
|
|
|
244
244
|
// This is a clean miss instead of a throw.
|
|
245
245
|
if (!categoryIDs.length) return []
|
|
246
246
|
|
|
247
|
-
const origin = latLngToCell(center.latitude, center.longitude, POI_H3_RESOLUTION)
|
|
247
|
+
const origin = latLngToCell(center.latitude, center.longitude, POI_H3_RESOLUTION)
|
|
248
248
|
const seenCells = new Set<string>()
|
|
249
249
|
let rows: POIRow[] = []
|
|
250
250
|
|
|
@@ -252,12 +252,12 @@ export class POILookup<DB extends POIDatabase = POIDatabase> extends SQLiteLooku
|
|
|
252
252
|
for (let ring = 0; ring < maxRings; ring++) {
|
|
253
253
|
// gridDisk(origin, ring) returns the whole disk out to `ring`, so diffing against
|
|
254
254
|
// the already-probed set derives only this ring's new cells.
|
|
255
|
-
const diskCells = gridDisk(origin, ring)
|
|
255
|
+
const diskCells = gridDisk(origin, ring)
|
|
256
256
|
const newCells = diskCells.filter((cell) => !seenCells.has(cell))
|
|
257
257
|
|
|
258
258
|
for (const cell of newCells) {
|
|
259
259
|
seenCells.add(cell)
|
|
260
|
-
const shortCell = shortCellToInt(cell
|
|
260
|
+
const shortCell = shortCellToInt(cell)
|
|
261
261
|
|
|
262
262
|
for (const categoryID of categoryIDs) {
|
|
263
263
|
rows.push(...allRows<POIRow>(this.#categoryCellProbe, shortCell, categoryID, limit))
|
|
@@ -344,8 +344,8 @@ export class POILookup<DB extends POIDatabase = POIDatabase> extends SQLiteLooku
|
|
|
344
344
|
function sortByDistance(rows: POIRow[], center: { latitude: number; longitude: number }): POIRow[] {
|
|
345
345
|
return [...rows].toSorted(
|
|
346
346
|
(a, b) =>
|
|
347
|
-
|
|
348
|
-
|
|
347
|
+
haversine(center.latitude, center.longitude, a.latitude, a.longitude) -
|
|
348
|
+
haversine(center.latitude, center.longitude, b.latitude, b.longitude)
|
|
349
349
|
)
|
|
350
350
|
}
|
|
351
351
|
|
|
@@ -363,7 +363,7 @@ function toHit(
|
|
|
363
363
|
country: row.country,
|
|
364
364
|
confidence: row.confidence,
|
|
365
365
|
gersID: row.gers_id,
|
|
366
|
-
distanceM: center ?
|
|
366
|
+
distanceM: center ? haversine(center.latitude, center.longitude, row.latitude, row.longitude) * 1000 : null,
|
|
367
367
|
}
|
|
368
368
|
}
|
|
369
369
|
|
|
@@ -11,14 +11,15 @@ import type { CandidateTable } from "#candidate/schema"
|
|
|
11
11
|
*
|
|
12
12
|
* `is_primary` is optional, so an older artifact without the column skips the re-rank without an error.
|
|
13
13
|
*/
|
|
14
|
-
export type PrimaryPreferenceRow = Pick<CandidateTable, "neg_rank"
|
|
14
|
+
export type PrimaryPreferenceRow = Pick<CandidateTable, "neg_rank"> &
|
|
15
15
|
Partial<Pick<CandidateTable, "is_primary" | "placetype_id" | "population" | "name_role">>
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* The log10-population penalty applied to an alias row when
|
|
19
|
-
*
|
|
18
|
+
* The log10-population penalty applied to an alias row when another row holds
|
|
19
|
+
* the same key as its primary name.
|
|
20
20
|
*
|
|
21
|
-
* An alias at least 10 times as populous still wins, so alternate names such as "LA"
|
|
21
|
+
* An alias at least 10 times as populous still wins, so alternate names such as "LA"
|
|
22
|
+
* and misspellings of a large city such as "Dehli" keep working.
|
|
22
23
|
*/
|
|
23
24
|
export const PRIMARY_PREFERENCE_LOG10 = 1
|
|
24
25
|
|
|
@@ -37,7 +38,7 @@ export const RERANK_FETCH = 64
|
|
|
37
38
|
*/
|
|
38
39
|
export type RankedRow<R> = R & {
|
|
39
40
|
/**
|
|
40
|
-
* `neg_rank` plus any
|
|
41
|
+
* `neg_rank` plus any alias penalty.
|
|
41
42
|
*
|
|
42
43
|
* Rows are ordered by this value.
|
|
43
44
|
* The emitted `prominence` derives from it.
|
|
@@ -47,12 +48,11 @@ export type RankedRow<R> = R & {
|
|
|
47
48
|
effectiveNegRank: number
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
|
-
* True when this row is
|
|
51
|
-
* the top primary for the same key.
|
|
51
|
+
* True when this row is an alias that the penalty moved behind the top primary for the same key.
|
|
52
52
|
*
|
|
53
53
|
* A demoted row loses `exactMatch`, so the resolver's country pin cannot promote
|
|
54
|
-
* a coincidental
|
|
55
|
-
* A dominant alias
|
|
54
|
+
* a coincidental exonym back over the primary.
|
|
55
|
+
* A dominant alias is never demoted.
|
|
56
56
|
*/
|
|
57
57
|
demoted: boolean
|
|
58
58
|
|
|
@@ -82,7 +82,7 @@ export type RankedRow<R> = R & {
|
|
|
82
82
|
/**
|
|
83
83
|
* Re-ranks population-ordered candidate rows.
|
|
84
84
|
*
|
|
85
|
-
*
|
|
85
|
+
* An alias must beat the top primary by `delta` in log10 population, in any country.
|
|
86
86
|
* The function returns the top `limit` rows.
|
|
87
87
|
*
|
|
88
88
|
* When `placetypes` is given, an exact tie prefers a populated `locality`.
|
|
@@ -103,20 +103,13 @@ export function rankByPrimaryPreference<R extends PrimaryPreferenceRow>(
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
const wouldPenalize = (r: R): boolean =>
|
|
109
|
-
typeof topCountry === "number" && r.is_primary !== 1 && r.country_id !== topCountry
|
|
110
|
-
|
|
111
|
-
const isCrossCountryAlias = (r: R): boolean =>
|
|
112
|
-
wouldPenalize(r) && !(exemptVariantAliases && r.name_role === "variant")
|
|
106
|
+
const isAliasOfPrimary = (r: R): boolean => topPrimary != null && r.is_primary !== 1
|
|
113
107
|
|
|
114
108
|
const annotate = (r: R): RankedRow<R> => {
|
|
115
|
-
const
|
|
109
|
+
const exempted = exemptVariantAliases && r.name_role === "variant" && isAliasOfPrimary(r)
|
|
110
|
+
const penalized = isAliasOfPrimary(r) && !exempted
|
|
116
111
|
const effectiveNegRank = r.neg_rank + (penalized ? delta : 0)
|
|
117
112
|
|
|
118
|
-
const exempted = !penalized && exemptVariantAliases && r.name_role === "variant" && wouldPenalize(r)
|
|
119
|
-
|
|
120
113
|
return {
|
|
121
114
|
...r,
|
|
122
115
|
effectiveNegRank,
|
package/lib/proximity-rerank.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* ordering silently discarded downstream.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import {
|
|
26
|
+
import { haversine } from "@mailwoman/spatial/geometries/point/distance"
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Full magnitude of the nearness term at distance 0, before decay.
|
|
@@ -92,7 +92,7 @@ export function combinedProminence(candidate: ProximityRerankable, bias: readonl
|
|
|
92
92
|
// A 0,0 pair interpreted as a real coordinate would receive an enormous score.
|
|
93
93
|
if (!(candidate.lat === 0 && candidate.lon === 0)) {
|
|
94
94
|
for (const b of bias) {
|
|
95
|
-
const d =
|
|
95
|
+
const d = haversine(b.lat, b.lon, candidate.lat, candidate.lon)
|
|
96
96
|
const term = (BIAS_BOOST * (b.weight ?? 1)) / (1 + d / PROX_SCALE_KM)
|
|
97
97
|
|
|
98
98
|
if (term > proxTerm) {
|
package/lib/reverse.ts
CHANGED
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
|
|
25
25
|
import { tryParsingJSON } from "@mailwoman/core/json"
|
|
26
26
|
import { allRows } from "@mailwoman/core/utils"
|
|
27
|
-
import {
|
|
27
|
+
import type { ParsedGeometry } from "@mailwoman/spatial"
|
|
28
|
+
import { bboxAround } from "@mailwoman/spatial/bbox"
|
|
29
|
+
import { haversine } from "@mailwoman/spatial/geometries/point/distance"
|
|
30
|
+
import { geometryContains } from "@mailwoman/spatial/geometries/polygon"
|
|
28
31
|
import { DatabaseClient } from "@mailwoman/sqlite/client"
|
|
29
32
|
import { tableExists } from "@mailwoman/sqlite/introspection"
|
|
30
33
|
import type { PathBuilderLike } from "path-ts"
|
|
@@ -311,7 +314,7 @@ export class WOFReverseGeocoder implements Disposable {
|
|
|
311
314
|
let bestKm = Infinity
|
|
312
315
|
|
|
313
316
|
for (const c of pointOnly) {
|
|
314
|
-
const km =
|
|
317
|
+
const km = haversine(lat, lon, c.lat, c.lon)
|
|
315
318
|
|
|
316
319
|
if (km < bestKm) {
|
|
317
320
|
bestKm = km
|
|
@@ -325,7 +328,7 @@ export class WOFReverseGeocoder implements Disposable {
|
|
|
325
328
|
// Approximate descent into finer tiers than the winner.
|
|
326
329
|
let current = winner
|
|
327
330
|
let currentConfirmed = winnerConfirmed
|
|
328
|
-
let currentDistanceKm = currentConfirmed ? null :
|
|
331
|
+
let currentDistanceKm = currentConfirmed ? null : haversine(lat, lon, current.lat, current.lon)
|
|
329
332
|
|
|
330
333
|
for (const tier of DESCENT_TIERS) {
|
|
331
334
|
if (placetypeDepth(tier) <= placetypeDepth(current.placetype)) continue
|
|
@@ -349,7 +352,7 @@ export class WOFReverseGeocoder implements Disposable {
|
|
|
349
352
|
}
|
|
350
353
|
|
|
351
354
|
if (contains === false) continue // known not-here, polygon rejected
|
|
352
|
-
const km =
|
|
355
|
+
const km = haversine(lat, lon, k.lat, k.lon)
|
|
353
356
|
|
|
354
357
|
if (km <= maxApproximateKm && km < bestKm) {
|
|
355
358
|
bestKm = km
|
|
@@ -494,7 +497,7 @@ export class WOFReverseGeocoder implements Disposable {
|
|
|
494
497
|
if (!this.#polygons) return null
|
|
495
498
|
const cached = this.#geometryCache.get(id)
|
|
496
499
|
|
|
497
|
-
if (cached
|
|
500
|
+
if (cached) return cached
|
|
498
501
|
|
|
499
502
|
if (this.#geometryCache.size >= WOFReverseGeocoder.#GEOMETRY_CACHE_CAP) {
|
|
500
503
|
this.#geometryCache.clear()
|
package/lib/schema.ts
CHANGED
|
@@ -170,23 +170,6 @@ export interface ConcordancesTable {
|
|
|
170
170
|
lastmodified: number
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
/**
|
|
174
|
-
* `coincident_roles`.
|
|
175
|
-
*
|
|
176
|
-
* The dual-role relation, where a place is both an admin region and a locality (Berlin the city-state).
|
|
177
|
-
*
|
|
178
|
-
* One row per (admin, locality) pair the resolver can complete a hierarchy with.
|
|
179
|
-
* Surfaced by {@link MailwomanLookupLike.coincidentRolesFor}.
|
|
180
|
-
*/
|
|
181
|
-
export interface CoincidentRolesTable {
|
|
182
|
-
admin_id: number
|
|
183
|
-
locality_id: number
|
|
184
|
-
relationship_type: string
|
|
185
|
-
admin_placetype: string
|
|
186
|
-
distance_km: number
|
|
187
|
-
locality_population: number
|
|
188
|
-
}
|
|
189
|
-
|
|
190
173
|
/**
|
|
191
174
|
* The full schema we hand to `Kysely<WOFDatabase>` / `new DatabaseClient<WOFDatabase>(...)`.
|
|
192
175
|
*
|
|
@@ -205,5 +188,4 @@ export interface WOFDatabase {
|
|
|
205
188
|
place_population: PlacePopulationTable
|
|
206
189
|
place_abbr: PlaceAbbrTable
|
|
207
190
|
concordances: ConcordancesTable
|
|
208
|
-
coincident_roles: CoincidentRolesTable
|
|
209
191
|
}
|
package/lib/search-fetch.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { allRows } from "@mailwoman/core/utils"
|
|
16
|
-
import { bboxAround } from "@mailwoman/spatial"
|
|
16
|
+
import { bboxAround } from "@mailwoman/spatial/bbox"
|
|
17
17
|
import type { DatabaseClient, SQLInputValue } from "@mailwoman/sqlite/client"
|
|
18
18
|
|
|
19
19
|
import { PLACE_BBOX_TABLE, PLACE_POPULATION_TABLE } from "#fts"
|
|
@@ -142,7 +142,7 @@ export function fetchSearchRows<DB>(options: {
|
|
|
142
142
|
params.push(query.country)
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
if (query.parentID
|
|
145
|
+
if (query.parentID) {
|
|
146
146
|
where.push(`(spr.parent_id = ? OR spr.id IN (SELECT id FROM ${sch}.ancestors WHERE ancestor_id = ?))`)
|
|
147
147
|
params.push(query.parentID, query.parentID)
|
|
148
148
|
}
|
|
@@ -152,7 +152,7 @@ export function fetchSearchRows<DB>(options: {
|
|
|
152
152
|
// A requested filter on a legacy extract is silently treated as no bbox filter
|
|
153
153
|
// so the query does not crash.
|
|
154
154
|
const extractHasBbox = hasBboxIndex.get(sch) === true
|
|
155
|
-
const useBboxJoin = (query.bbox || query.near?.maxDistanceKm
|
|
155
|
+
const useBboxJoin = (query.bbox || query.near?.maxDistanceKm) && extractHasBbox
|
|
156
156
|
let joinClause = `JOIN ${sch}.spr ON spr.id = place_search.wof_id`
|
|
157
157
|
|
|
158
158
|
if (useBboxJoin) {
|
|
@@ -35,7 +35,7 @@ export class SqliteConventionSource<DB> implements ConventionSource {
|
|
|
35
35
|
get(wofID: number): Convention | null {
|
|
36
36
|
const cached = this.#cache.get(wofID)
|
|
37
37
|
|
|
38
|
-
if (cached
|
|
38
|
+
if (cached) return cached
|
|
39
39
|
let value: Convention | null = null
|
|
40
40
|
|
|
41
41
|
try {
|
package/lib/street/centroid.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { StreetCentroidHit, StreetCentroidLookup } from "@mailwoman/core/resolver"
|
|
9
|
-
import {
|
|
9
|
+
import { haversine } from "@mailwoman/spatial/geometries/point/distance"
|
|
10
10
|
import { DatabaseClient } from "@mailwoman/sqlite/client"
|
|
11
11
|
import type { PathBuilderLike } from "path-ts"
|
|
12
12
|
|
|
@@ -51,7 +51,7 @@ const AGG_SELECT =
|
|
|
51
51
|
* Half the bounding-box diagonal in meters, used as a coarse uncertainty radius for a street centroid.
|
|
52
52
|
*/
|
|
53
53
|
function extentRadiusM(minLat: number, maxLat: number, minLon: number, maxLon: number): number {
|
|
54
|
-
return Math.round(
|
|
54
|
+
return Math.round(haversine(minLat, minLon, maxLat, maxLon) * 500)
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|