@mailwoman/resolver-wof-sqlite 10.1.0 → 10.2.1
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/README.md +1 -0
- package/lib/address/point/interpolation.ts +6 -2
- package/lib/address/point.ts +15 -14
- package/lib/build/candidate.ts +11 -21
- package/lib/build/slim.ts +1 -1
- package/lib/candidate/importance.ts +1 -1
- package/lib/candidate/lookup.ts +47 -34
- package/lib/candidate/own-name.ts +1 -1
- package/lib/candidate/scoring.ts +29 -29
- package/lib/capitals.ts +3 -3
- package/lib/coincident-roles.ts +15 -4
- package/lib/convention/schema.ts +2 -9
- package/lib/convention.ts +7 -7
- package/lib/coverage-manifest-schema.ts +6 -6
- package/lib/currency-backfill.ts +13 -13
- package/lib/exact-match.ts +1 -1
- package/lib/extracts.ts +1 -1
- package/lib/fst/autocomplete.ts +2 -2
- package/lib/fst/builder.ts +9 -7
- package/lib/fst/deserialize-web.ts +8 -8
- package/lib/fst/freshness.ts +27 -26
- package/lib/fst/matcher.ts +5 -3
- package/lib/fst/serialize.ts +12 -17
- package/lib/fst/types.ts +14 -23
- package/lib/fst.ts +1 -0
- package/lib/fts.ts +1 -1
- package/lib/geonames/aliases.ts +3 -3
- package/lib/geonames/postal.ts +1 -1
- package/lib/index.ts +3 -2
- package/lib/interpolation.ts +7 -6
- package/lib/lookup.ts +26 -20
- package/lib/nsul/lookup.ts +1 -1
- package/lib/place-importance-schema.ts +5 -5
- package/lib/poi/lookup.ts +13 -21
- package/lib/postcode-point-lookup.ts +1 -10
- package/lib/primary-preference.ts +2 -2
- package/lib/proximity-rerank.ts +1 -1
- package/lib/ranking-weights.ts +15 -10
- package/lib/region-keys.ts +2 -2
- package/lib/reverse.ts +45 -18
- package/lib/schema.ts +3 -12
- package/lib/sqlite/convention-source.ts +3 -3
- package/lib/sqlite/utils.ts +1 -1
- package/lib/street/centroid.ts +1 -1
- package/lib/street/morphology/fst/builder.ts +16 -7
- package/lib/street/morphology/fst/loader.ts +8 -7
- package/lib/street/name-lookup.ts +4 -8
- package/lib/street/normalize.ts +3 -3
- package/lib/street/segment-schema.ts +2 -2
- package/lib/types.ts +17 -12
- package/lib/uprn/lookup.ts +1 -1
- package/lib/weights-overlay-linker.ts +38 -26
- package/out/address/point/interpolation.d.ts +1 -0
- package/out/address/point/interpolation.d.ts.map +1 -1
- package/out/address/point/interpolation.js +5 -2
- package/out/address/point/interpolation.js.map +1 -1
- package/out/address/point.d.ts +5 -4
- package/out/address/point.d.ts.map +1 -1
- package/out/address/point.js +11 -10
- package/out/address/point.js.map +1 -1
- package/out/build/candidate.d.ts +7 -17
- package/out/build/candidate.d.ts.map +1 -1
- package/out/build/candidate.js +4 -11
- package/out/build/candidate.js.map +1 -1
- package/out/build/slim.js +1 -1
- package/out/build/slim.js.map +1 -1
- package/out/candidate/importance.js +1 -1
- package/out/candidate/importance.js.map +1 -1
- package/out/candidate/lookup.d.ts +8 -5
- package/out/candidate/lookup.d.ts.map +1 -1
- package/out/candidate/lookup.js +35 -25
- package/out/candidate/lookup.js.map +1 -1
- package/out/candidate/own-name.js +1 -1
- package/out/candidate/own-name.js.map +1 -1
- package/out/candidate/scoring.js +27 -25
- package/out/candidate/scoring.js.map +1 -1
- package/out/capitals.d.ts +1 -1
- package/out/capitals.d.ts.map +1 -1
- package/out/capitals.js.map +1 -1
- package/out/coincident-roles.d.ts +4 -4
- package/out/coincident-roles.d.ts.map +1 -1
- package/out/coincident-roles.js +11 -0
- package/out/coincident-roles.js.map +1 -1
- package/out/convention/schema.d.ts +2 -8
- package/out/convention/schema.d.ts.map +1 -1
- package/out/convention/schema.js.map +1 -1
- package/out/convention.d.ts +4 -4
- package/out/convention.d.ts.map +1 -1
- package/out/convention.js +3 -3
- package/out/convention.js.map +1 -1
- package/out/coverage-manifest-schema.d.ts +1 -1
- package/out/coverage-manifest-schema.d.ts.map +1 -1
- package/out/coverage-manifest-schema.js +5 -5
- package/out/coverage-manifest-schema.js.map +1 -1
- package/out/currency-backfill.d.ts +6 -6
- package/out/currency-backfill.d.ts.map +1 -1
- package/out/currency-backfill.js +7 -7
- package/out/currency-backfill.js.map +1 -1
- package/out/exact-match.js +1 -1
- package/out/exact-match.js.map +1 -1
- package/out/extracts.d.ts +1 -1
- package/out/extracts.d.ts.map +1 -1
- package/out/extracts.js.map +1 -1
- package/out/fst/autocomplete.d.ts +1 -1
- package/out/fst/autocomplete.d.ts.map +1 -1
- package/out/fst/autocomplete.js +1 -1
- package/out/fst/autocomplete.js.map +1 -1
- package/out/fst/builder.d.ts.map +1 -1
- package/out/fst/builder.js +9 -7
- package/out/fst/builder.js.map +1 -1
- package/out/fst/deserialize-web.d.ts +1 -1
- package/out/fst/deserialize-web.d.ts.map +1 -1
- package/out/fst/deserialize-web.js +7 -7
- package/out/fst/deserialize-web.js.map +1 -1
- package/out/fst/freshness.d.ts +9 -9
- package/out/fst/freshness.d.ts.map +1 -1
- package/out/fst/freshness.js +19 -18
- package/out/fst/freshness.js.map +1 -1
- package/out/fst/matcher.d.ts +4 -3
- package/out/fst/matcher.d.ts.map +1 -1
- package/out/fst/matcher.js.map +1 -1
- package/out/fst/serialize.d.ts +1 -5
- package/out/fst/serialize.d.ts.map +1 -1
- package/out/fst/serialize.js +11 -15
- package/out/fst/serialize.js.map +1 -1
- package/out/fst/types.d.ts +14 -22
- package/out/fst/types.d.ts.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/fts.js +1 -1
- package/out/fts.js.map +1 -1
- package/out/geonames/aliases.d.ts +1 -1
- package/out/geonames/aliases.d.ts.map +1 -1
- package/out/geonames/aliases.js +2 -2
- package/out/geonames/aliases.js.map +1 -1
- package/out/geonames/postal.js +1 -1
- package/out/geonames/postal.js.map +1 -1
- package/out/index.d.ts +3 -2
- package/out/index.d.ts.map +1 -1
- package/out/index.js +1 -0
- package/out/index.js.map +1 -1
- package/out/interpolation.d.ts +4 -4
- package/out/interpolation.d.ts.map +1 -1
- package/out/interpolation.js +4 -3
- package/out/interpolation.js.map +1 -1
- package/out/lookup.d.ts +4 -3
- package/out/lookup.d.ts.map +1 -1
- package/out/lookup.js +22 -14
- package/out/lookup.js.map +1 -1
- package/out/nsul/lookup.js.map +1 -1
- package/out/place-importance-schema.d.ts +2 -2
- package/out/place-importance-schema.d.ts.map +1 -1
- package/out/place-importance-schema.js +3 -3
- package/out/place-importance-schema.js.map +1 -1
- package/out/poi/lookup.d.ts +10 -13
- package/out/poi/lookup.d.ts.map +1 -1
- package/out/poi/lookup.js +3 -7
- package/out/poi/lookup.js.map +1 -1
- package/out/postcode-point-lookup.d.ts +1 -9
- package/out/postcode-point-lookup.d.ts.map +1 -1
- package/out/postcode-point-lookup.js.map +1 -1
- package/out/primary-preference.js +2 -2
- package/out/primary-preference.js.map +1 -1
- package/out/proximity-rerank.d.ts +1 -1
- package/out/proximity-rerank.d.ts.map +1 -1
- package/out/ranking-weights.d.ts +15 -10
- package/out/ranking-weights.d.ts.map +1 -1
- package/out/ranking-weights.js.map +1 -1
- package/out/region-keys.d.ts +2 -2
- package/out/region-keys.d.ts.map +1 -1
- package/out/region-keys.js.map +1 -1
- package/out/reverse.d.ts +7 -4
- package/out/reverse.d.ts.map +1 -1
- package/out/reverse.js +38 -13
- package/out/reverse.js.map +1 -1
- package/out/schema.d.ts +2 -11
- package/out/schema.d.ts.map +1 -1
- package/out/sqlite/convention-source.d.ts +1 -1
- package/out/sqlite/convention-source.d.ts.map +1 -1
- package/out/sqlite/convention-source.js +2 -2
- package/out/sqlite/convention-source.js.map +1 -1
- package/out/sqlite/utils.d.ts +1 -1
- package/out/sqlite/utils.d.ts.map +1 -1
- package/out/street/centroid.js +1 -1
- package/out/street/centroid.js.map +1 -1
- package/out/street/morphology/fst/builder.d.ts +3 -3
- package/out/street/morphology/fst/builder.d.ts.map +1 -1
- package/out/street/morphology/fst/builder.js +13 -4
- package/out/street/morphology/fst/builder.js.map +1 -1
- package/out/street/morphology/fst/loader.d.ts +4 -4
- package/out/street/morphology/fst/loader.d.ts.map +1 -1
- package/out/street/morphology/fst/loader.js +4 -3
- package/out/street/morphology/fst/loader.js.map +1 -1
- package/out/street/name-lookup.d.ts +2 -2
- package/out/street/name-lookup.d.ts.map +1 -1
- package/out/street/name-lookup.js +2 -4
- package/out/street/name-lookup.js.map +1 -1
- package/out/street/normalize.js +3 -3
- package/out/street/normalize.js.map +1 -1
- package/out/street/segment-schema.d.ts +2 -2
- package/out/street/segment-schema.js +2 -2
- package/out/types.d.ts +16 -12
- package/out/types.d.ts.map +1 -1
- package/out/uprn/lookup.js +1 -1
- package/out/uprn/lookup.js.map +1 -1
- package/out/weights-overlay-linker.d.ts +9 -6
- package/out/weights-overlay-linker.d.ts.map +1 -1
- package/out/weights-overlay-linker.js +21 -7
- package/out/weights-overlay-linker.js.map +1 -1
- package/package.json +13 -13
package/README.md
CHANGED
|
@@ -166,6 +166,7 @@ You can also build the index programmatically via the package's `./fts` subpath:
|
|
|
166
166
|
|
|
167
167
|
```ts
|
|
168
168
|
import { DatabaseSync } from "node:sqlite"
|
|
169
|
+
|
|
169
170
|
import { buildPlaceSearchFts } from "@mailwoman/resolver-wof-sqlite/fts"
|
|
170
171
|
|
|
171
172
|
using db = new DatabaseSync("/path/to/wof.db")
|
|
@@ -107,6 +107,8 @@ export class AddressPointInterpolator<
|
|
|
107
107
|
this.#resources = resources.move()
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
readonly radiusCalibration = null
|
|
111
|
+
|
|
110
112
|
find(query: InterpolationQuery): InterpolatedHit | null {
|
|
111
113
|
const numberRaw = query.number.trim()
|
|
112
114
|
|
|
@@ -168,8 +170,8 @@ function interpolateFromNeighbors(rows: readonly PointRow[], n: number): Interpo
|
|
|
168
170
|
|
|
169
171
|
// Nearest known number below and above the query.
|
|
170
172
|
// The rows never contain n itself.
|
|
171
|
-
let below: NumberAnchor |
|
|
172
|
-
let above: NumberAnchor |
|
|
173
|
+
let below: NumberAnchor | null = null
|
|
174
|
+
let above: NumberAnchor | null = null
|
|
173
175
|
|
|
174
176
|
for (const anchor of anchors) {
|
|
175
177
|
if (anchor.n < n) {
|
|
@@ -190,6 +192,7 @@ function interpolateFromNeighbors(rows: readonly PointRow[], n: number): Interpo
|
|
|
190
192
|
lon: below.lon + (above.lon - below.lon) * t,
|
|
191
193
|
interpolated: true,
|
|
192
194
|
method: "address_point",
|
|
195
|
+
parityMatched: null,
|
|
193
196
|
bracket: "both",
|
|
194
197
|
uncertaintyM: Math.round(spanM / 2),
|
|
195
198
|
source: below.source,
|
|
@@ -217,6 +220,7 @@ function interpolateFromNeighbors(rows: readonly PointRow[], n: number): Interpo
|
|
|
217
220
|
lon,
|
|
218
221
|
interpolated: true,
|
|
219
222
|
method: "address_point",
|
|
223
|
+
parityMatched: null,
|
|
220
224
|
bracket: "single",
|
|
221
225
|
// Explicitly larger than the both-sided radius: the whole pair span plus the overshoot.
|
|
222
226
|
uncertaintyM: Math.round(pairM + overshootM),
|
package/lib/address/point.ts
CHANGED
|
@@ -76,13 +76,14 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
|
|
|
76
76
|
* @param dbPath Extract path.
|
|
77
77
|
* @param opts.streetLocale The street-normalization locale the extract was built with.
|
|
78
78
|
* A mismatch makes every key miss.
|
|
79
|
-
* It defaults to `"us"`.
|
|
80
79
|
* @param opts.localityKeys Whether the extract's `locality_norm` holds full place names.
|
|
81
80
|
* With `"abbreviated"`, a row's locality can select a row but never rejects one.
|
|
82
81
|
* The US situs extract uses NAD city fields.
|
|
83
82
|
* Some counties abbreviate them (`addi` for Addison).
|
|
84
83
|
* Other counties replace them with the parent town.
|
|
85
|
-
*
|
|
84
|
+
*
|
|
85
|
+
* @defaultValue `"us"`
|
|
86
|
+
* @defaultValue `"abbreviated"` for `"us"` and `"full"` for every other locale.
|
|
86
87
|
*/
|
|
87
88
|
constructor(
|
|
88
89
|
dbPath: PathBuilderLike,
|
|
@@ -133,7 +134,7 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
|
|
|
133
134
|
locality?: string
|
|
134
135
|
region?: string
|
|
135
136
|
subregion?: string
|
|
136
|
-
bbox
|
|
137
|
+
bbox: { minLat: number; maxLat: number; minLon: number; maxLon: number } | null
|
|
137
138
|
}): AddressPointHit | null {
|
|
138
139
|
if (!this.#byPostcode || !this.#byLocality || !this.#byBbox) return null
|
|
139
140
|
const number = normalizeHouseNumberForKey(query.number, this.#locale)
|
|
@@ -154,7 +155,7 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
|
|
|
154
155
|
// `streetKeyVariants` yields the literal key first.
|
|
155
156
|
// Each variant runs the full number fallback sequence.
|
|
156
157
|
// The first variant with a match wins.
|
|
157
|
-
let row: AddressPointRow |
|
|
158
|
+
let row: AddressPointRow | null = null
|
|
158
159
|
|
|
159
160
|
for (const streetNorm of streetKeyVariants(query.street, streetLocaleForSurface(query.street, this.#locale))) {
|
|
160
161
|
row = this.#findForKey(streetNorm, number, scoped)
|
|
@@ -169,8 +170,8 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
|
|
|
169
170
|
lon: row.lon,
|
|
170
171
|
source: row.source,
|
|
171
172
|
release: row.release,
|
|
172
|
-
|
|
173
|
-
|
|
173
|
+
localityNorm: row.locality_norm || null,
|
|
174
|
+
postcode: row.postcode || null,
|
|
174
175
|
}
|
|
175
176
|
}
|
|
176
177
|
|
|
@@ -187,7 +188,7 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
|
|
|
187
188
|
postcode?: string
|
|
188
189
|
locality?: string
|
|
189
190
|
localityTail?: NameKey
|
|
190
|
-
bbox
|
|
191
|
+
bbox: { minLat: number; maxLat: number; minLon: number; maxLon: number } | null
|
|
191
192
|
}
|
|
192
193
|
) {
|
|
193
194
|
let row = this.#probe(streetNorm, number, query)
|
|
@@ -241,22 +242,22 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
|
|
|
241
242
|
postcode?: string
|
|
242
243
|
locality?: string
|
|
243
244
|
localityTail?: NameKey
|
|
244
|
-
bbox
|
|
245
|
+
bbox: { minLat: number; maxLat: number; minLon: number; maxLon: number } | null
|
|
245
246
|
}
|
|
246
|
-
): AddressPointRow |
|
|
247
|
-
let row: AddressPointRow |
|
|
247
|
+
): AddressPointRow | null {
|
|
248
|
+
let row: AddressPointRow | null = null
|
|
248
249
|
|
|
249
250
|
if (query.postcode) {
|
|
250
251
|
const postcode = query.postcode.trim()
|
|
251
|
-
const localityKey = query.locality ? this.#localityKey(query.locality) :
|
|
252
|
+
const localityKey = query.locality ? this.#localityKey(query.locality) : null
|
|
252
253
|
|
|
253
254
|
// One postcode can cover several places with the same street and number,
|
|
254
255
|
// so a row whose locality agrees with the query is tried first.
|
|
255
256
|
// The postcode-only row answers only if `#scopeContradicts` accepts it.
|
|
256
|
-
const agreeing = localityKey ? this.#byPostcodeLocality!(postcode, localityKey, streetNorm, number) :
|
|
257
|
+
const agreeing = localityKey ? this.#byPostcodeLocality!(postcode, localityKey, streetNorm, number) : null
|
|
257
258
|
const candidate = agreeing ?? this.#byPostcode!(postcode, streetNorm, number)
|
|
258
259
|
|
|
259
|
-
row = candidate && !this.#scopeContradicts(candidate, query) ? candidate :
|
|
260
|
+
row = candidate && !this.#scopeContradicts(candidate, query) ? candidate : null
|
|
260
261
|
}
|
|
261
262
|
|
|
262
263
|
if (!row && query.locality) {
|
|
@@ -274,7 +275,7 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
|
|
|
274
275
|
const candidate = this.#byBbox!(streetNorm, number, b.minLat, b.maxLat, b.minLon, b.maxLon)
|
|
275
276
|
|
|
276
277
|
// A row with its own scope that the scoped probes missed is a different address in the same box.
|
|
277
|
-
row = candidate && !this.#scopeContradicts(candidate, query) ? candidate :
|
|
278
|
+
row = candidate && !this.#scopeContradicts(candidate, query) ? candidate : null
|
|
278
279
|
}
|
|
279
280
|
|
|
280
281
|
return row
|
package/lib/build/candidate.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { COUNTRY_POPULATION } from "@mailwoman/codex/country"
|
|
8
|
-
import { pathExists } from "@mailwoman/core/fs/readers"
|
|
8
|
+
import { pathExists } from "@mailwoman/core/fs/readers/stat"
|
|
9
9
|
import { removePath } from "@mailwoman/core/fs/writers"
|
|
10
10
|
import { stringifyJSON } from "@mailwoman/core/json"
|
|
11
11
|
import { DatabaseClient } from "@mailwoman/sqlite/client"
|
|
@@ -32,19 +32,6 @@ import { resurrectCurrencyHoles } from "#currency-backfill"
|
|
|
32
32
|
import type { WOFDatabase } from "#schema"
|
|
33
33
|
import { normalizeLocalityForKey } from "#street/normalize"
|
|
34
34
|
|
|
35
|
-
/**
|
|
36
|
-
* Re-exports the country display-name staging pass from the candidate build's public module.
|
|
37
|
-
*/
|
|
38
|
-
export { stageCountryDisplayNames } from "#candidate/country-display-names"
|
|
39
|
-
/**
|
|
40
|
-
* Re-exports the gloss detector's excluded placetypes and default key-count threshold.
|
|
41
|
-
*/
|
|
42
|
-
export { GLOSS_EXCLUDED_PLACETYPES, GLOSS_KEY_THRESHOLD } from "#candidate/name-roles"
|
|
43
|
-
/**
|
|
44
|
-
* Re-exports the per-place record that every candidate staging pass writes its rows from.
|
|
45
|
-
*/
|
|
46
|
-
export type { PlaceAttrs } from "#candidate/place-attrs"
|
|
47
|
-
|
|
48
35
|
/**
|
|
49
36
|
* Options for {@link buildCandidateTable}.
|
|
50
37
|
*/
|
|
@@ -111,7 +98,8 @@ export interface BuildCandidateOptions {
|
|
|
111
98
|
|
|
112
99
|
/**
|
|
113
100
|
* The deprecated placetypes eligible for restoration.
|
|
114
|
-
*
|
|
101
|
+
*
|
|
102
|
+
* @defaultValue `locality`
|
|
115
103
|
*/
|
|
116
104
|
deadPlacetypes?: readonly string[]
|
|
117
105
|
}
|
|
@@ -123,9 +111,10 @@ export interface BuildCandidateOptions {
|
|
|
123
111
|
|
|
124
112
|
/**
|
|
125
113
|
* The key-count threshold for the gloss anomaly detector.
|
|
126
|
-
* The default is {@link GLOSS_KEY_THRESHOLD}.
|
|
127
114
|
*
|
|
128
115
|
* Tests override it with a value sized for fixtures.
|
|
116
|
+
*
|
|
117
|
+
* @defaultValue {@linkcode GLOSS_KEY_THRESHOLD}
|
|
129
118
|
*/
|
|
130
119
|
glossKeyThreshold?: number
|
|
131
120
|
}
|
|
@@ -178,9 +167,9 @@ export interface BuildCandidateResult {
|
|
|
178
167
|
/**
|
|
179
168
|
* The count of places that received an `importance` score.
|
|
180
169
|
*
|
|
181
|
-
* It is `
|
|
170
|
+
* It is `null` when no score source was given.
|
|
182
171
|
*/
|
|
183
|
-
importanceScored
|
|
172
|
+
importanceScored: number | null
|
|
184
173
|
|
|
185
174
|
/**
|
|
186
175
|
* The count of places that matched a scored group by name key, country and placetype
|
|
@@ -188,7 +177,7 @@ export interface BuildCandidateResult {
|
|
|
188
177
|
*
|
|
189
178
|
* A large change between rebuilds means the score source and the admin source have diverged.
|
|
190
179
|
*/
|
|
191
|
-
importanceFiltered
|
|
180
|
+
importanceFiltered: number | null
|
|
192
181
|
|
|
193
182
|
/**
|
|
194
183
|
* The count of alias rows the gloss anomaly detector stamped `name_role = 'gloss'`.
|
|
@@ -257,7 +246,7 @@ export async function buildCandidateTable(opts: BuildCandidateOptions): Promise<
|
|
|
257
246
|
return id
|
|
258
247
|
}
|
|
259
248
|
|
|
260
|
-
let importance: ReturnType<typeof loadImportanceIndex> |
|
|
249
|
+
let importance: ReturnType<typeof loadImportanceIndex> | null = null
|
|
261
250
|
|
|
262
251
|
if (opts.importance) {
|
|
263
252
|
progress("importance", `loading place_importance from ${opts.importance}`)
|
|
@@ -603,6 +592,7 @@ export async function buildCandidateTable(opts: BuildCandidateOptions): Promise<
|
|
|
603
592
|
ancestorPlaces: sidecar.ancestorPlaces + nLocalityScoped,
|
|
604
593
|
intervalPlaces: sidecar.intervalPlaces,
|
|
605
594
|
...roles,
|
|
606
|
-
|
|
595
|
+
importanceScored: importance ? importance.matched : null,
|
|
596
|
+
importanceFiltered: importance ? importance.refused : null,
|
|
607
597
|
}
|
|
608
598
|
}
|
package/lib/build/slim.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { statPath, pathExists } from "@mailwoman/core/fs/readers"
|
|
7
|
+
import { statPath, pathExists } from "@mailwoman/core/fs/readers/stat"
|
|
8
8
|
import { temporaryDirectory } from "@mailwoman/core/fs/temporary"
|
|
9
9
|
import { copyFileTo, removePath } from "@mailwoman/core/fs/writers"
|
|
10
10
|
import { stringifyJSON } from "@mailwoman/core/json"
|
package/lib/candidate/lookup.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { expandPlacetypeFilter } from "@mailwoman/codex/placetype-map"
|
|
8
|
+
import type { VariantAliasExemption } from "@mailwoman/core/geocode"
|
|
8
9
|
import { type Ancestor, type GazetteerArtifactCoverage, referentialFromPopulation } from "@mailwoman/core/resolver"
|
|
9
10
|
import { allRows } from "@mailwoman/core/utils"
|
|
10
11
|
import { jaroWinkler, levenshteinSimilarity } from "@mailwoman/match/comparators"
|
|
@@ -31,9 +32,6 @@ import { hasColumn, hasTable } from "#sqlite-utils"
|
|
|
31
32
|
import { type NameKey, normalizeLocalityForKey, stripLocalityQualifier } from "#street/normalize"
|
|
32
33
|
import type { FindPlaceQuery, PlaceCandidate, PlaceLookup, WOFPlacetype } from "#types"
|
|
33
34
|
|
|
34
|
-
export { rankByPrimaryPreference } from "#primary-preference"
|
|
35
|
-
export type { RankedRow } from "#primary-preference"
|
|
36
|
-
|
|
37
35
|
/**
|
|
38
36
|
* Where a {@link WOFCandidateTableLookup} reads from
|
|
39
37
|
* (a read-only `candidate.db` or a caller-held connection) and how it ranks.
|
|
@@ -41,9 +39,13 @@ export type { RankedRow } from "#primary-preference"
|
|
|
41
39
|
export interface WOFCandidateTableLookupOpts extends SQLiteLookupOptions<CandidateDatabase> {
|
|
42
40
|
/**
|
|
43
41
|
* Exempt `name_role = 'variant'` aliases from the cross-country primary-preference penalty.
|
|
44
|
-
*
|
|
42
|
+
*
|
|
43
|
+
* - No-ops without the role column.
|
|
44
|
+
* - `"not_applied"` penalizes variant aliases like any other.
|
|
45
|
+
*
|
|
46
|
+
* @defaultValue `"applied"`.
|
|
45
47
|
*/
|
|
46
|
-
variantAliasExemption?:
|
|
48
|
+
variantAliasExemption?: VariantAliasExemption
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
type CandidateRow = Pick<
|
|
@@ -106,7 +108,7 @@ export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> imp
|
|
|
106
108
|
*/
|
|
107
109
|
readonly #nameKeyExistsProbe: ReturnType<DatabaseClient["prepare"]> | undefined
|
|
108
110
|
|
|
109
|
-
readonly artifactCoverage: GazetteerArtifactCoverage |
|
|
111
|
+
readonly artifactCoverage: GazetteerArtifactCoverage | null
|
|
110
112
|
|
|
111
113
|
readonly #importanceSelect: string
|
|
112
114
|
|
|
@@ -158,7 +160,7 @@ export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> imp
|
|
|
158
160
|
// Optional columns are detected once here for hot-path reads.
|
|
159
161
|
this.#importanceSelect = hasColumn(this.database, "candidate", "importance") ? ", importance" : ""
|
|
160
162
|
this.#hasNameRole = hasColumn(this.database, "candidate", "name_role")
|
|
161
|
-
this.#variantAliasExemption = opts.variantAliasExemption ===
|
|
163
|
+
this.#variantAliasExemption = (opts.variantAliasExemption ?? "applied") === "applied"
|
|
162
164
|
this.#roleSelect = this.#hasNameRole ? ", name_role" : ""
|
|
163
165
|
|
|
164
166
|
if (hasTable(this.database, CANDIDATE_ANCESTOR_TABLE)) {
|
|
@@ -227,7 +229,7 @@ export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> imp
|
|
|
227
229
|
return label
|
|
228
230
|
}
|
|
229
231
|
|
|
230
|
-
#qualifierRegionIDs(qualifier: string, country: string |
|
|
232
|
+
#qualifierRegionIDs(qualifier: string, country: string | null): Set<number> {
|
|
231
233
|
const ids = new Set<number>()
|
|
232
234
|
|
|
233
235
|
if (!this.#qualifierProbe) return ids
|
|
@@ -264,7 +266,7 @@ export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> imp
|
|
|
264
266
|
#applyAdminContainment(
|
|
265
267
|
rows: Array<RankedRow<CandidateRow>>,
|
|
266
268
|
qualifier: string,
|
|
267
|
-
country: string |
|
|
269
|
+
country: string | null,
|
|
268
270
|
opts: {
|
|
269
271
|
nameKey: NameKey
|
|
270
272
|
strippedKey: NameKey
|
|
@@ -425,8 +427,20 @@ export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> imp
|
|
|
425
427
|
country: query.country?.toUpperCase() ?? "",
|
|
426
428
|
lat: Number(hit.latitude),
|
|
427
429
|
lon: Number(hit.longitude),
|
|
430
|
+
parent_id: null,
|
|
428
431
|
score: 1,
|
|
432
|
+
distanceKm: null,
|
|
429
433
|
exactMatch: true,
|
|
434
|
+
prominence: null,
|
|
435
|
+
population: null,
|
|
436
|
+
referential: null,
|
|
437
|
+
encyclopedic: null,
|
|
438
|
+
importance: null,
|
|
439
|
+
bbox: null,
|
|
440
|
+
mismatch: null,
|
|
441
|
+
containedByQualifier: null,
|
|
442
|
+
regionScopeMiss: null,
|
|
443
|
+
variantAliasExempted: null,
|
|
430
444
|
},
|
|
431
445
|
]
|
|
432
446
|
}
|
|
@@ -482,13 +496,13 @@ export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> imp
|
|
|
482
496
|
filterParams.push(...shapeParams)
|
|
483
497
|
|
|
484
498
|
// Region scope is optional (`region_id = parentID`); the fallback stays unscoped.
|
|
485
|
-
const regionParentID = query.parentID ||
|
|
499
|
+
const regionParentID = query.parentID || null
|
|
486
500
|
|
|
487
|
-
const probe = (nk: string, regionID: number |
|
|
501
|
+
const probe = (nk: string, regionID: number | null, countryID?: number): Array<RankedRow<CandidateRow>> => {
|
|
488
502
|
const conds = ["name_key = ?", ...filters]
|
|
489
503
|
const params: Array<string | number> = [nk, ...filterParams]
|
|
490
504
|
|
|
491
|
-
if (regionID !==
|
|
505
|
+
if (regionID !== null) {
|
|
492
506
|
conds.push("region_id = ?")
|
|
493
507
|
params.push(regionID)
|
|
494
508
|
}
|
|
@@ -508,7 +522,7 @@ export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> imp
|
|
|
508
522
|
return rankByPrimaryPreference(fetched, limit, undefined, this.#idToPlacetype, this.#variantAliasExemption)
|
|
509
523
|
}
|
|
510
524
|
|
|
511
|
-
const cascade = (regionID: number |
|
|
525
|
+
const cascade = (regionID: number | null): Array<RankedRow<CandidateRow>> => {
|
|
512
526
|
let rows = probe(nameKey, regionID)
|
|
513
527
|
|
|
514
528
|
if (!rows.length) {
|
|
@@ -567,8 +581,8 @@ export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> imp
|
|
|
567
581
|
|
|
568
582
|
let regionScopeMiss = false
|
|
569
583
|
|
|
570
|
-
if (!rows.length && regionParentID !==
|
|
571
|
-
rows = cascade(
|
|
584
|
+
if (!rows.length && regionParentID !== null) {
|
|
585
|
+
rows = cascade(null)
|
|
572
586
|
regionScopeMiss = rows.length > 0
|
|
573
587
|
}
|
|
574
588
|
|
|
@@ -604,7 +618,7 @@ export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> imp
|
|
|
604
618
|
|
|
605
619
|
// Admin-containment re-rank runs last.
|
|
606
620
|
if (query.regionQualifier?.trim() && this.#qualifierProbe && this.#wantsLocality(query.placetype)) {
|
|
607
|
-
rows = this.#applyAdminContainment(rows, query.regionQualifier.trim(), query.country, {
|
|
621
|
+
rows = this.#applyAdminContainment(rows, query.regionQualifier.trim(), query.country ?? null, {
|
|
608
622
|
nameKey,
|
|
609
623
|
strippedKey: normalizeLocalityForKey(stripLocalityQualifier(text)),
|
|
610
624
|
shapeFilters,
|
|
@@ -616,6 +630,7 @@ export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> imp
|
|
|
616
630
|
const candidates = rows.map((row): PlaceCandidate => {
|
|
617
631
|
const hasBbox = row.min_lat != null && row.max_lat != null && row.min_lon != null && row.max_lon != null
|
|
618
632
|
const parent = this.#ancestorLineage(Number(row.spr_id))[0]
|
|
633
|
+
const hasPopulation = row.population !== null && row.population > 0
|
|
619
634
|
|
|
620
635
|
return {
|
|
621
636
|
id: Number(row.spr_id),
|
|
@@ -626,37 +641,35 @@ export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> imp
|
|
|
626
641
|
lat: Number(row.latitude),
|
|
627
642
|
lon: Number(row.longitude),
|
|
628
643
|
|
|
629
|
-
|
|
644
|
+
parent_id: parent ? Number(parent.id) : null,
|
|
630
645
|
|
|
631
646
|
score: -Number(row.neg_rank),
|
|
647
|
+
distanceKm: null,
|
|
632
648
|
|
|
633
649
|
prominence: -Number(row.effectiveNegRank),
|
|
634
650
|
|
|
635
651
|
exactMatch: !row.demoted && !row.fuzzy,
|
|
636
652
|
|
|
637
|
-
|
|
653
|
+
regionScopeMiss: regionScopeMiss ? true : null,
|
|
638
654
|
|
|
639
|
-
|
|
655
|
+
containedByQualifier: row.containedByQualifier ?? null,
|
|
640
656
|
|
|
641
|
-
|
|
657
|
+
variantAliasExempted: row.variantExempted ? true : null,
|
|
642
658
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
659
|
+
population: hasPopulation ? row.population : null,
|
|
660
|
+
referential: hasPopulation ? referentialFromPopulation(row.population) : null,
|
|
661
|
+
encyclopedic: null,
|
|
646
662
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
: {}),
|
|
650
|
-
...(hasBbox
|
|
663
|
+
importance: typeof row.importance === "number" && Number.isFinite(row.importance) ? row.importance : null,
|
|
664
|
+
bbox: hasBbox
|
|
651
665
|
? {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
maxLon: Number(row.max_lon),
|
|
657
|
-
},
|
|
666
|
+
minLat: Number(row.min_lat),
|
|
667
|
+
maxLat: Number(row.max_lat),
|
|
668
|
+
minLon: Number(row.min_lon),
|
|
669
|
+
maxLon: Number(row.max_lon),
|
|
658
670
|
}
|
|
659
|
-
:
|
|
671
|
+
: null,
|
|
672
|
+
mismatch: null,
|
|
660
673
|
}
|
|
661
674
|
})
|
|
662
675
|
|
|
@@ -135,7 +135,7 @@ export function ownNameSimilarity(primaryKey: string, aliasKey: string): number
|
|
|
135
135
|
const primary = romanizeNameKey(primaryKey)
|
|
136
136
|
const alias = romanizeNameKey(aliasKey)
|
|
137
137
|
|
|
138
|
-
if (primary
|
|
138
|
+
if (!primary || !alias || !primary || !alias) return null
|
|
139
139
|
|
|
140
140
|
return levenshteinSimilarity(expandNameAbbreviations(primary), expandNameAbbreviations(alias))
|
|
141
141
|
}
|
package/lib/candidate/scoring.ts
CHANGED
|
@@ -56,7 +56,7 @@ export function candidateFromSearchRow(
|
|
|
56
56
|
|
|
57
57
|
// Proximity boost applies only when the query includes `near` and the candidate has real coordinates.
|
|
58
58
|
// The decay is tunable via proximityBoost + proximityScaleKm.
|
|
59
|
-
let distanceKm: number |
|
|
59
|
+
let distanceKm: number | null = null
|
|
60
60
|
// The best decayed-distance term over `near` and every `bias` point wins, each scaled by its weight.
|
|
61
61
|
let proximityTerm = 0
|
|
62
62
|
|
|
@@ -96,6 +96,14 @@ export function candidateFromSearchRow(
|
|
|
96
96
|
// additive units, so hints can win a cross-country postcode tie without a hard filter.
|
|
97
97
|
const prominence = popTerm + proximityTerm
|
|
98
98
|
|
|
99
|
+
const population = row.population !== null && row.population > 0 ? row.population : null
|
|
100
|
+
|
|
101
|
+
// The referential key derives from this row's population and no other field,
|
|
102
|
+
// so it cannot drift from the ordering.
|
|
103
|
+
// Consumers read the encyclopedic value.
|
|
104
|
+
// No ranking site reads it.
|
|
105
|
+
// The bbox gives parity with the wasm lookup.
|
|
106
|
+
// Without it the region→bbox constraint is dead and disambiguation falls to population ranking.
|
|
99
107
|
const candidate: PlaceCandidate = {
|
|
100
108
|
id: row.id,
|
|
101
109
|
prominence,
|
|
@@ -104,35 +112,27 @@ export function candidateFromSearchRow(
|
|
|
104
112
|
country: row.country ?? "",
|
|
105
113
|
lat: row.lat ?? 0,
|
|
106
114
|
lon: row.lon ?? 0,
|
|
107
|
-
parent_id: row.parent_id ??
|
|
115
|
+
parent_id: row.parent_id ?? null,
|
|
108
116
|
score,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// region→bbox constraint is dead and disambiguation falls to population ranking.
|
|
129
|
-
if (row.min_latitude != null && row.max_latitude != null && row.min_longitude != null && row.max_longitude != null) {
|
|
130
|
-
candidate.bbox = {
|
|
131
|
-
minLat: row.min_latitude,
|
|
132
|
-
maxLat: row.max_latitude,
|
|
133
|
-
minLon: row.min_longitude,
|
|
134
|
-
maxLon: row.max_longitude,
|
|
135
|
-
}
|
|
117
|
+
distanceKm,
|
|
118
|
+
exactMatch: null,
|
|
119
|
+
population,
|
|
120
|
+
referential: population === null ? null : referentialFromPopulation(population),
|
|
121
|
+
encyclopedic: row.encyclopedic ?? null,
|
|
122
|
+
importance: null,
|
|
123
|
+
bbox:
|
|
124
|
+
row.min_latitude != null && row.max_latitude != null && row.min_longitude != null && row.max_longitude != null
|
|
125
|
+
? {
|
|
126
|
+
minLat: row.min_latitude,
|
|
127
|
+
maxLat: row.max_latitude,
|
|
128
|
+
minLon: row.min_longitude,
|
|
129
|
+
maxLon: row.max_longitude,
|
|
130
|
+
}
|
|
131
|
+
: null,
|
|
132
|
+
mismatch: null,
|
|
133
|
+
containedByQualifier: null,
|
|
134
|
+
regionScopeMiss: null,
|
|
135
|
+
variantAliasExempted: null,
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
return candidate
|
package/lib/capitals.ts
CHANGED
|
@@ -108,9 +108,9 @@ export class CapitalIndex {
|
|
|
108
108
|
* an unknown country, or no matching entry.
|
|
109
109
|
*/
|
|
110
110
|
levelOfPlace(
|
|
111
|
-
name: string | null
|
|
112
|
-
country: string | null
|
|
113
|
-
latitude: number | null
|
|
111
|
+
name: string | null,
|
|
112
|
+
country: string | null,
|
|
113
|
+
latitude: number | null,
|
|
114
114
|
longitude: number | null | undefined
|
|
115
115
|
): CapitalLevel {
|
|
116
116
|
if (!name || !country || typeof latitude !== "number" || typeof longitude !== "number") return CAPITAL_LEVEL.none
|
package/lib/coincident-roles.ts
CHANGED
|
@@ -66,25 +66,25 @@ export interface BuildCoincidentRolesOpts {
|
|
|
66
66
|
/**
|
|
67
67
|
* Drop + rebuild the table if it already exists.
|
|
68
68
|
*
|
|
69
|
-
*
|
|
69
|
+
* @defaultValue `true` (the build is cheap + idempotent)
|
|
70
70
|
*/
|
|
71
71
|
drop?: boolean
|
|
72
72
|
/**
|
|
73
73
|
* Relative tolerance: a pair is kept when centroid distance ≤ `toleranceFraction × bbox-diagonal`.
|
|
74
74
|
*
|
|
75
|
-
*
|
|
75
|
+
* @defaultValue `0.15`
|
|
76
76
|
*/
|
|
77
77
|
toleranceFraction?: number
|
|
78
78
|
/**
|
|
79
79
|
* Floor (km) under the relative tolerance, so small-bbox city-states still qualify.
|
|
80
80
|
*
|
|
81
|
-
*
|
|
81
|
+
* @defaultValue `12`
|
|
82
82
|
*/
|
|
83
83
|
minToleranceKm?: number
|
|
84
84
|
/**
|
|
85
85
|
* Centroid distance (km) below which a region-tier pair is classed `city-state` (metadata only).
|
|
86
86
|
*
|
|
87
|
-
*
|
|
87
|
+
* @defaultValue `2`
|
|
88
88
|
*/
|
|
89
89
|
cityStateMaxKm?: number
|
|
90
90
|
onProgress?: (phase: string, detail?: string) => void
|
|
@@ -320,6 +320,17 @@ export function loadCoincidentLocalities<DB>(db: DatabaseClient<DB>): Map<number
|
|
|
320
320
|
relationshipType: r.relationshipType,
|
|
321
321
|
population: r.population,
|
|
322
322
|
distanceKm: r.distanceKm,
|
|
323
|
+
parent_id: null,
|
|
324
|
+
prominence: null,
|
|
325
|
+
referential: null,
|
|
326
|
+
encyclopedic: null,
|
|
327
|
+
importance: null,
|
|
328
|
+
exactMatch: null,
|
|
329
|
+
mismatch: null,
|
|
330
|
+
resolutionQuality: null,
|
|
331
|
+
containedByQualifier: null,
|
|
332
|
+
regionScopeMiss: null,
|
|
333
|
+
variantAliasExempted: null,
|
|
323
334
|
}
|
|
324
335
|
|
|
325
336
|
const list = map.get(r.adminID)
|
package/lib/convention/schema.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* other.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import type { KeyValueMetaTable } from "@mailwoman/sqlite/database-schema"
|
|
12
13
|
import type { Kysely } from "kysely"
|
|
13
14
|
|
|
14
15
|
import { ADDRESS_CONVENTION_TABLE } from "#convention"
|
|
@@ -30,17 +31,9 @@ export interface AddressConventionTable {
|
|
|
30
31
|
source: string
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
/**
|
|
34
|
-
* Key/value provenance for the sealed artifact.
|
|
35
|
-
*/
|
|
36
|
-
export interface ConventionMetaTable {
|
|
37
|
-
key: string
|
|
38
|
-
value: string | null
|
|
39
|
-
}
|
|
40
|
-
|
|
41
34
|
export interface ConventionDatabase {
|
|
42
35
|
address_convention: AddressConventionTable
|
|
43
|
-
meta:
|
|
36
|
+
meta: KeyValueMetaTable
|
|
44
37
|
}
|
|
45
38
|
|
|
46
39
|
/**
|
package/lib/convention.ts
CHANGED
|
@@ -79,10 +79,10 @@ export type Strategy = (query: FindPlaceQuery, convention: ResolvedConvention) =
|
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
81
|
* Looks up a convention by WOF polygon id.
|
|
82
|
-
* `get` returns `
|
|
82
|
+
* `get` returns `null` when the polygon has no override.
|
|
83
83
|
*/
|
|
84
84
|
export interface ConventionSource {
|
|
85
|
-
get(wofID: number): Convention |
|
|
85
|
+
get(wofID: number): Convention | null
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/**
|
|
@@ -95,8 +95,8 @@ export class SeedConventionSource implements ConventionSource {
|
|
|
95
95
|
this.#rows = new Map(Object.entries(rows).map(([k, v]) => [Number(k), v]))
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
get(wofID: number): Convention |
|
|
99
|
-
return this.#rows.get(wofID)
|
|
98
|
+
get(wofID: number): Convention | null {
|
|
99
|
+
return this.#rows.get(wofID) ?? null
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
@@ -106,7 +106,7 @@ export class SeedConventionSource implements ConventionSource {
|
|
|
106
106
|
* A layer's `candidateStrategies` replaces the whole list.
|
|
107
107
|
* `scoringWeights` merges key by key, so a layer can change one weight without restating the others.
|
|
108
108
|
*/
|
|
109
|
-
export function mergeConventions(base: Convention, ...overrides: Array<Convention | undefined>): Convention {
|
|
109
|
+
export function mergeConventions(base: Convention, ...overrides: Array<Convention | null | undefined>): Convention {
|
|
110
110
|
const out: Convention = {
|
|
111
111
|
candidateStrategies: base.candidateStrategies ? [...base.candidateStrategies] : undefined,
|
|
112
112
|
scoringWeights: base.scoringWeights ? { ...base.scoringWeights } : undefined,
|
|
@@ -115,11 +115,11 @@ export function mergeConventions(base: Convention, ...overrides: Array<Conventio
|
|
|
115
115
|
for (const o of overrides) {
|
|
116
116
|
if (!o) continue
|
|
117
117
|
|
|
118
|
-
if (o.candidateStrategies
|
|
118
|
+
if (o.candidateStrategies) {
|
|
119
119
|
out.candidateStrategies = [...o.candidateStrategies]
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
if (o.scoringWeights
|
|
122
|
+
if (o.scoringWeights) {
|
|
123
123
|
out.scoringWeights = { ...(out.scoringWeights ?? WORLD_DEFAULT.scoringWeights), ...o.scoringWeights }
|
|
124
124
|
}
|
|
125
125
|
}
|