@mailwoman/resolver-wof-sqlite 10.0.0 → 10.2.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/README.md +9 -8
- package/lib/address/point/interpolation.ts +45 -39
- package/lib/address/point/schema.ts +17 -40
- package/lib/address/point.ts +316 -0
- package/lib/{address/index.ts → address.ts} +1 -1
- package/lib/ancestry/backfill.ts +20 -86
- package/lib/{ancestry/index.ts → ancestry.ts} +19 -15
- package/lib/build/candidate.ts +122 -174
- package/lib/build/population-corrections.ts +64 -0
- package/lib/build/slim.ts +44 -114
- package/lib/candidate/alias-bags.ts +16 -17
- package/lib/candidate/ancestors/schema.ts +77 -61
- package/lib/candidate/ancestors/sidecar.ts +15 -22
- package/lib/candidate/country-display-names.ts +6 -20
- package/lib/candidate/extract-fold.ts +10 -55
- package/lib/candidate/fts.ts +5 -23
- package/lib/candidate/importance.ts +65 -68
- package/lib/candidate/lookup.ts +124 -415
- package/lib/candidate/name-roles.ts +15 -33
- package/lib/candidate/own-name.ts +35 -32
- package/lib/candidate/place-attrs.ts +20 -13
- package/lib/candidate/schema.ts +29 -65
- package/lib/candidate/scoring.ts +54 -93
- package/lib/capital-schema.ts +21 -17
- package/lib/capitals.ts +29 -27
- package/lib/coincident-roles.ts +85 -55
- package/lib/convention/schema.ts +15 -16
- package/lib/convention.ts +144 -0
- package/lib/coverage-manifest-schema.ts +40 -45
- package/lib/currency-backfill.ts +34 -79
- package/lib/env.ts +1 -1
- package/lib/exact-match.ts +32 -21
- package/lib/extracts.ts +97 -74
- package/lib/fst/autocomplete.ts +54 -37
- package/lib/fst/builder.ts +37 -41
- package/lib/fst/deserialize-web.ts +16 -17
- package/lib/fst/format.ts +41 -23
- package/lib/fst/freshness.ts +43 -120
- package/lib/fst/matcher.ts +7 -5
- package/lib/fst/serialize.ts +54 -74
- package/lib/fst/types.ts +78 -75
- package/lib/{fst/index.ts → fst.ts} +1 -0
- package/lib/fts/query.ts +5 -36
- package/lib/fts.ts +280 -0
- package/lib/geonames/aliases.ts +73 -144
- package/lib/geonames/postal.ts +162 -71
- package/lib/index.ts +9 -8
- package/lib/interpolation.ts +48 -100
- package/lib/lookup.ts +155 -270
- package/lib/name-score.ts +9 -7
- package/lib/nsul/lookup.ts +37 -54
- package/lib/nsul/schema.ts +16 -43
- package/lib/paths.ts +159 -0
- package/lib/place-importance-schema.ts +62 -169
- package/lib/poi/lookup.ts +104 -121
- package/lib/poi/schema.ts +42 -26
- package/lib/polygon-schema.ts +10 -6
- package/lib/postal/city/alias/lookup.ts +22 -40
- package/lib/postal/city/alias/schema.ts +14 -10
- package/lib/postal/city/candidate-schema.ts +26 -20
- package/lib/postcode-point-lookup.ts +8 -27
- package/lib/primary-preference.ts +46 -126
- package/lib/proximity-rerank.ts +44 -33
- package/lib/ranking-weights.ts +105 -78
- package/lib/region-keys.ts +70 -42
- package/lib/reverse.ts +149 -86
- package/lib/schema.ts +73 -50
- package/lib/search-fetch.ts +90 -68
- package/lib/sqlite/convention-source.ts +15 -23
- package/lib/sqlite/utils.ts +19 -22
- package/lib/street/centroid/schema.ts +37 -42
- package/lib/street/{centroid/index.ts → centroid.ts} +16 -25
- package/lib/street/morphology/fst/builder.ts +60 -42
- package/lib/street/morphology/fst/loader.ts +40 -32
- package/lib/street/name-lookup.ts +45 -41
- package/lib/street/normalize.ts +88 -219
- package/lib/street/segment-schema.ts +116 -43
- package/lib/{street/index.ts → street.ts} +1 -1
- package/lib/types.ts +109 -150
- package/lib/unified-schema.ts +29 -29
- package/lib/uprn/existence.ts +30 -19
- package/lib/uprn/lookup.ts +29 -68
- package/lib/uprn/schema.ts +18 -40
- package/lib/weights-overlay-linker.ts +236 -252
- package/out/address/point/interpolation.d.ts +20 -23
- package/out/address/point/interpolation.d.ts.map +1 -1
- package/out/address/point/interpolation.js +42 -39
- package/out/address/point/interpolation.js.map +1 -1
- package/out/address/point/schema.d.ts +12 -35
- package/out/address/point/schema.d.ts.map +1 -1
- package/out/address/point/schema.js +3 -19
- package/out/address/point/schema.js.map +1 -1
- package/out/address/point.d.ts +57 -0
- package/out/address/point.d.ts.map +1 -0
- package/out/address/point.js +206 -0
- package/out/address/point.js.map +1 -0
- package/out/{address/index.js → address.d.ts} +2 -2
- package/out/address.d.ts.map +1 -0
- package/out/{address/index.d.ts → address.js} +2 -2
- package/out/address.js.map +1 -0
- package/out/ancestry/backfill.d.ts +11 -64
- package/out/ancestry/backfill.d.ts.map +1 -1
- package/out/ancestry/backfill.js +14 -74
- package/out/ancestry/backfill.js.map +1 -1
- package/out/ancestry.d.ts +49 -0
- package/out/ancestry.d.ts.map +1 -0
- package/out/ancestry.js +59 -0
- package/out/ancestry.js.map +1 -0
- package/out/build/candidate.d.ts +82 -112
- package/out/build/candidate.d.ts.map +1 -1
- package/out/build/candidate.js +17 -100
- package/out/build/candidate.js.map +1 -1
- package/out/build/population-corrections.d.ts +52 -0
- package/out/build/population-corrections.d.ts.map +1 -0
- package/out/build/population-corrections.js +40 -0
- package/out/build/population-corrections.js.map +1 -0
- package/out/build/slim.d.ts +8 -44
- package/out/build/slim.d.ts.map +1 -1
- package/out/build/slim.js +33 -94
- package/out/build/slim.js.map +1 -1
- package/out/candidate/alias-bags.d.ts +6 -6
- package/out/candidate/alias-bags.d.ts.map +1 -1
- package/out/candidate/alias-bags.js +16 -17
- package/out/candidate/alias-bags.js.map +1 -1
- package/out/candidate/ancestors/schema.d.ts +73 -57
- package/out/candidate/ancestors/schema.d.ts.map +1 -1
- package/out/candidate/ancestors/schema.js +56 -48
- package/out/candidate/ancestors/schema.js.map +1 -1
- package/out/candidate/ancestors/sidecar.d.ts +3 -8
- package/out/candidate/ancestors/sidecar.d.ts.map +1 -1
- package/out/candidate/ancestors/sidecar.js +15 -22
- package/out/candidate/ancestors/sidecar.js.map +1 -1
- package/out/candidate/country-display-names.d.ts +5 -15
- package/out/candidate/country-display-names.d.ts.map +1 -1
- package/out/candidate/country-display-names.js +6 -20
- package/out/candidate/country-display-names.js.map +1 -1
- package/out/candidate/extract-fold.d.ts +7 -15
- package/out/candidate/extract-fold.d.ts.map +1 -1
- package/out/candidate/extract-fold.js +5 -47
- package/out/candidate/extract-fold.js.map +1 -1
- package/out/candidate/fts.d.ts +5 -23
- package/out/candidate/fts.d.ts.map +1 -1
- package/out/candidate/fts.js +5 -23
- package/out/candidate/fts.js.map +1 -1
- package/out/candidate/importance.d.ts +60 -65
- package/out/candidate/importance.d.ts.map +1 -1
- package/out/candidate/importance.js +56 -63
- package/out/candidate/importance.js.map +1 -1
- package/out/candidate/lookup.d.ts +18 -55
- package/out/candidate/lookup.d.ts.map +1 -1
- package/out/candidate/lookup.js +82 -385
- package/out/candidate/lookup.js.map +1 -1
- package/out/candidate/name-roles.d.ts +9 -21
- package/out/candidate/name-roles.d.ts.map +1 -1
- package/out/candidate/name-roles.js +15 -33
- package/out/candidate/name-roles.js.map +1 -1
- package/out/candidate/own-name.d.ts +23 -25
- package/out/candidate/own-name.d.ts.map +1 -1
- package/out/candidate/own-name.js +35 -32
- package/out/candidate/own-name.js.map +1 -1
- package/out/candidate/place-attrs.d.ts +20 -13
- package/out/candidate/place-attrs.d.ts.map +1 -1
- package/out/candidate/place-attrs.js +4 -5
- package/out/candidate/place-attrs.js.map +1 -1
- package/out/candidate/schema.d.ts +27 -62
- package/out/candidate/schema.d.ts.map +1 -1
- package/out/candidate/schema.js +10 -22
- package/out/candidate/schema.js.map +1 -1
- package/out/candidate/scoring.d.ts +7 -8
- package/out/candidate/scoring.d.ts.map +1 -1
- package/out/candidate/scoring.js +52 -89
- package/out/candidate/scoring.js.map +1 -1
- package/out/capital-schema.d.ts +21 -17
- package/out/capital-schema.d.ts.map +1 -1
- package/out/capital-schema.js +17 -15
- package/out/capital-schema.js.map +1 -1
- package/out/capitals.d.ts +27 -25
- package/out/capitals.d.ts.map +1 -1
- package/out/capitals.js +23 -21
- package/out/capitals.js.map +1 -1
- package/out/coincident-roles.d.ts +60 -43
- package/out/coincident-roles.d.ts.map +1 -1
- package/out/coincident-roles.js +72 -50
- package/out/coincident-roles.js.map +1 -1
- package/out/convention/schema.d.ts +14 -14
- package/out/convention/schema.d.ts.map +1 -1
- package/out/convention/schema.js +6 -4
- package/out/convention/schema.js.map +1 -1
- package/out/convention.d.ts +97 -0
- package/out/convention.d.ts.map +1 -0
- package/out/convention.js +81 -0
- package/out/convention.js.map +1 -0
- package/out/coverage-manifest-schema.d.ts +34 -39
- package/out/coverage-manifest-schema.d.ts.map +1 -1
- package/out/coverage-manifest-schema.js +25 -34
- package/out/coverage-manifest-schema.js.map +1 -1
- package/out/currency-backfill.d.ts +16 -38
- package/out/currency-backfill.d.ts.map +1 -1
- package/out/currency-backfill.js +19 -46
- package/out/currency-backfill.js.map +1 -1
- package/out/env.d.ts +1 -1
- package/out/env.js +1 -1
- package/out/exact-match.d.ts +19 -10
- package/out/exact-match.d.ts.map +1 -1
- package/out/exact-match.js +32 -21
- package/out/exact-match.js.map +1 -1
- package/out/extracts.d.ts +66 -55
- package/out/extracts.d.ts.map +1 -1
- package/out/extracts.js +63 -55
- package/out/extracts.js.map +1 -1
- package/out/fst/autocomplete.d.ts +29 -21
- package/out/fst/autocomplete.d.ts.map +1 -1
- package/out/fst/autocomplete.js +39 -28
- package/out/fst/autocomplete.js.map +1 -1
- package/out/fst/builder.d.ts.map +1 -1
- package/out/fst/builder.js +37 -41
- package/out/fst/builder.js.map +1 -1
- package/out/fst/deserialize-web.d.ts +4 -4
- package/out/fst/deserialize-web.d.ts.map +1 -1
- package/out/fst/deserialize-web.js +15 -16
- package/out/fst/deserialize-web.js.map +1 -1
- package/out/fst/format.d.ts +41 -23
- package/out/fst/format.d.ts.map +1 -1
- package/out/fst/format.js +41 -23
- package/out/fst/format.js.map +1 -1
- package/out/fst/freshness.d.ts +19 -81
- package/out/fst/freshness.d.ts.map +1 -1
- package/out/fst/freshness.js +31 -94
- package/out/fst/freshness.js.map +1 -1
- package/out/fst/matcher.d.ts +5 -4
- package/out/fst/matcher.d.ts.map +1 -1
- package/out/fst/matcher.js +2 -2
- package/out/fst/matcher.js.map +1 -1
- package/out/fst/serialize.d.ts +28 -28
- package/out/fst/serialize.d.ts.map +1 -1
- package/out/fst/serialize.js +53 -71
- package/out/fst/serialize.js.map +1 -1
- package/out/fst/types.d.ts +78 -74
- package/out/fst/types.d.ts.map +1 -1
- package/out/fst/types.js +1 -4
- package/out/fst/types.js.map +1 -1
- package/out/{fst/index.js → fst.d.ts} +2 -1
- package/out/fst.d.ts.map +1 -0
- package/out/{fst/index.d.ts → fst.js} +2 -1
- package/out/fst.js.map +1 -0
- package/out/fts/query.d.ts +2 -27
- package/out/fts/query.d.ts.map +1 -1
- package/out/fts/query.js +5 -36
- package/out/fts/query.js.map +1 -1
- package/out/fts.d.ts +126 -0
- package/out/fts.d.ts.map +1 -0
- package/out/fts.js +188 -0
- package/out/fts.js.map +1 -0
- package/out/geonames/aliases.d.ts +26 -62
- package/out/geonames/aliases.d.ts.map +1 -1
- package/out/geonames/aliases.js +45 -117
- package/out/geonames/aliases.js.map +1 -1
- package/out/geonames/postal.d.ts +84 -44
- package/out/geonames/postal.d.ts.map +1 -1
- package/out/geonames/postal.js +115 -73
- package/out/geonames/postal.js.map +1 -1
- package/out/{geonames/index.js → geonames.d.ts} +1 -1
- package/out/geonames.d.ts.map +1 -0
- package/out/{geonames/index.d.ts → geonames.js} +1 -1
- package/out/geonames.js.map +1 -0
- package/out/index.d.ts +9 -8
- package/out/index.d.ts.map +1 -1
- package/out/index.js +6 -5
- package/out/index.js.map +1 -1
- package/out/interpolation.d.ts +18 -55
- package/out/interpolation.d.ts.map +1 -1
- package/out/interpolation.js +27 -69
- package/out/interpolation.js.map +1 -1
- package/out/lookup.d.ts +23 -51
- package/out/lookup.d.ts.map +1 -1
- package/out/lookup.js +136 -234
- package/out/lookup.js.map +1 -1
- package/out/name-score.d.ts +2 -2
- package/out/name-score.d.ts.map +1 -1
- package/out/name-score.js +9 -7
- package/out/name-score.js.map +1 -1
- package/out/nsul/lookup.d.ts +33 -34
- package/out/nsul/lookup.d.ts.map +1 -1
- package/out/nsul/lookup.js +26 -40
- package/out/nsul/lookup.js.map +1 -1
- package/out/nsul/schema.d.ts +16 -43
- package/out/nsul/schema.d.ts.map +1 -1
- package/out/nsul/schema.js +9 -35
- package/out/nsul/schema.js.map +1 -1
- package/out/{nsul/index.d.ts → nsul.d.ts} +1 -1
- package/out/nsul.d.ts.map +1 -0
- package/out/{nsul/index.js → nsul.js} +1 -1
- package/out/nsul.js.map +1 -0
- package/out/paths.d.ts +124 -0
- package/out/paths.d.ts.map +1 -0
- package/out/paths.js +104 -0
- package/out/paths.js.map +1 -0
- package/out/place-importance-schema.d.ts +51 -143
- package/out/place-importance-schema.d.ts.map +1 -1
- package/out/place-importance-schema.js +28 -143
- package/out/place-importance-schema.js.map +1 -1
- package/out/poi/lookup.d.ts +40 -47
- package/out/poi/lookup.d.ts.map +1 -1
- package/out/poi/lookup.js +69 -96
- package/out/poi/lookup.js.map +1 -1
- package/out/poi/schema.d.ts +41 -25
- package/out/poi/schema.d.ts.map +1 -1
- package/out/poi/schema.js +25 -17
- package/out/poi/schema.js.map +1 -1
- package/out/{poi/index.js → poi.d.ts} +1 -1
- package/out/poi.d.ts.map +1 -0
- package/out/{poi/index.d.ts → poi.js} +1 -1
- package/out/poi.js.map +1 -0
- package/out/polygon-schema.d.ts +10 -6
- package/out/polygon-schema.d.ts.map +1 -1
- package/out/polygon-schema.js +2 -2
- package/out/polygon-schema.js.map +1 -1
- package/out/postal/city/alias/lookup.d.ts +20 -23
- package/out/postal/city/alias/lookup.d.ts.map +1 -1
- package/out/postal/city/alias/lookup.js +17 -30
- package/out/postal/city/alias/lookup.js.map +1 -1
- package/out/postal/city/alias/schema.d.ts +14 -10
- package/out/postal/city/alias/schema.d.ts.map +1 -1
- package/out/postal/city/alias/schema.js +10 -8
- package/out/postal/city/alias/schema.js.map +1 -1
- package/out/postal/city/candidate-schema.d.ts +25 -19
- package/out/postal/city/candidate-schema.d.ts.map +1 -1
- package/out/postal/city/candidate-schema.js +21 -17
- package/out/postal/city/candidate-schema.js.map +1 -1
- package/out/{postal/index.d.ts → postal.d.ts} +1 -1
- package/out/postal.d.ts.map +1 -0
- package/out/{postal/index.js → postal.js} +1 -1
- package/out/postal.js.map +1 -0
- package/out/postcode-point-lookup.d.ts +6 -24
- package/out/postcode-point-lookup.d.ts.map +1 -1
- package/out/postcode-point-lookup.js +5 -17
- package/out/postcode-point-lookup.js.map +1 -1
- package/out/primary-preference.d.ts +38 -92
- package/out/primary-preference.d.ts.map +1 -1
- package/out/primary-preference.js +15 -99
- package/out/primary-preference.js.map +1 -1
- package/out/proximity-rerank.d.ts +41 -30
- package/out/proximity-rerank.d.ts.map +1 -1
- package/out/proximity-rerank.js +38 -29
- package/out/proximity-rerank.js.map +1 -1
- package/out/ranking-weights.d.ts +98 -64
- package/out/ranking-weights.d.ts.map +1 -1
- package/out/ranking-weights.js +26 -25
- package/out/ranking-weights.js.map +1 -1
- package/out/region-keys.d.ts +50 -31
- package/out/region-keys.d.ts.map +1 -1
- package/out/region-keys.js +68 -40
- package/out/region-keys.js.map +1 -1
- package/out/reverse.d.ts +78 -50
- package/out/reverse.d.ts.map +1 -1
- package/out/reverse.js +98 -61
- package/out/reverse.js.map +1 -1
- package/out/schema.d.ts +72 -49
- package/out/schema.d.ts.map +1 -1
- package/out/schema.js +4 -5
- package/out/schema.js.map +1 -1
- package/out/search-fetch.d.ts +16 -12
- package/out/search-fetch.d.ts.map +1 -1
- package/out/search-fetch.js +83 -63
- package/out/search-fetch.js.map +1 -1
- package/out/sqlite/convention-source.d.ts +12 -16
- package/out/sqlite/convention-source.d.ts.map +1 -1
- package/out/sqlite/convention-source.js +13 -22
- package/out/sqlite/convention-source.js.map +1 -1
- package/out/sqlite/utils.d.ts +19 -19
- package/out/sqlite/utils.d.ts.map +1 -1
- package/out/sqlite/utils.js +14 -15
- package/out/sqlite/utils.js.map +1 -1
- package/out/street/centroid/schema.d.ts +37 -42
- package/out/street/centroid/schema.d.ts.map +1 -1
- package/out/street/centroid/schema.js +9 -21
- package/out/street/centroid/schema.js.map +1 -1
- package/out/street/centroid.d.ts +30 -0
- package/out/street/centroid.d.ts.map +1 -0
- package/out/street/{centroid/index.js → centroid.js} +13 -24
- package/out/street/centroid.js.map +1 -0
- package/out/street/morphology/fst/builder.d.ts +20 -10
- package/out/street/morphology/fst/builder.d.ts.map +1 -1
- package/out/street/morphology/fst/builder.js +44 -34
- package/out/street/morphology/fst/builder.js.map +1 -1
- package/out/street/morphology/fst/loader.d.ts +33 -26
- package/out/street/morphology/fst/loader.d.ts.map +1 -1
- package/out/street/morphology/fst/loader.js +23 -22
- package/out/street/morphology/fst/loader.js.map +1 -1
- package/out/street/name-lookup.d.ts +24 -18
- package/out/street/name-lookup.d.ts.map +1 -1
- package/out/street/name-lookup.js +34 -35
- package/out/street/name-lookup.js.map +1 -1
- package/out/street/normalize.d.ts +35 -136
- package/out/street/normalize.d.ts.map +1 -1
- package/out/street/normalize.js +77 -194
- package/out/street/normalize.js.map +1 -1
- package/out/street/segment-schema.d.ts +79 -39
- package/out/street/segment-schema.d.ts.map +1 -1
- package/out/street/segment-schema.js +71 -21
- package/out/street/segment-schema.js.map +1 -1
- package/out/{street/index.js → street.d.ts} +2 -2
- package/out/street.d.ts.map +1 -0
- package/out/{street/index.d.ts → street.js} +2 -2
- package/out/street.js.map +1 -0
- package/out/types.d.ts +84 -149
- package/out/types.d.ts.map +1 -1
- package/out/types.js +0 -6
- package/out/types.js.map +1 -1
- package/out/unified-schema.d.ts +8 -12
- package/out/unified-schema.d.ts.map +1 -1
- package/out/unified-schema.js +29 -29
- package/out/unified-schema.js.map +1 -1
- package/out/uprn/existence.d.ts +28 -17
- package/out/uprn/existence.d.ts.map +1 -1
- package/out/uprn/existence.js +26 -16
- package/out/uprn/existence.js.map +1 -1
- package/out/uprn/lookup.d.ts +14 -44
- package/out/uprn/lookup.d.ts.map +1 -1
- package/out/uprn/lookup.js +21 -57
- package/out/uprn/lookup.js.map +1 -1
- package/out/uprn/schema.d.ts +18 -40
- package/out/uprn/schema.d.ts.map +1 -1
- package/out/uprn/schema.js +10 -32
- package/out/uprn/schema.js.map +1 -1
- package/out/{uprn/index.d.ts → uprn.d.ts} +1 -1
- package/out/uprn.d.ts.map +1 -0
- package/out/{uprn/index.js → uprn.js} +1 -1
- package/out/uprn.js.map +1 -0
- package/out/weights-overlay-linker.d.ts +138 -144
- package/out/weights-overlay-linker.d.ts.map +1 -1
- package/out/weights-overlay-linker.js +105 -159
- package/out/weights-overlay-linker.js.map +1 -1
- package/package.json +91 -596
- package/lib/address/point/index.ts +0 -328
- package/lib/convention/index.ts +0 -154
- package/lib/fts/index.ts +0 -361
- package/out/address/index.d.ts.map +0 -1
- package/out/address/index.js.map +0 -1
- package/out/address/point/index.d.ts +0 -53
- package/out/address/point/index.d.ts.map +0 -1
- package/out/address/point/index.js +0 -222
- package/out/address/point/index.js.map +0 -1
- package/out/ancestry/index.d.ts +0 -45
- package/out/ancestry/index.d.ts.map +0 -1
- package/out/ancestry/index.js +0 -55
- package/out/ancestry/index.js.map +0 -1
- package/out/convention/index.d.ts +0 -106
- package/out/convention/index.d.ts.map +0 -1
- package/out/convention/index.js +0 -93
- package/out/convention/index.js.map +0 -1
- package/out/fst/index.d.ts.map +0 -1
- package/out/fst/index.js.map +0 -1
- package/out/fts/index.d.ts +0 -184
- package/out/fts/index.d.ts.map +0 -1
- package/out/fts/index.js +0 -277
- package/out/fts/index.js.map +0 -1
- package/out/geonames/index.d.ts.map +0 -1
- package/out/geonames/index.js.map +0 -1
- package/out/nsul/index.d.ts.map +0 -1
- package/out/nsul/index.js.map +0 -1
- package/out/poi/index.d.ts.map +0 -1
- package/out/poi/index.js.map +0 -1
- package/out/postal/index.d.ts.map +0 -1
- package/out/postal/index.js.map +0 -1
- package/out/street/centroid/index.d.ts +0 -40
- package/out/street/centroid/index.d.ts.map +0 -1
- package/out/street/centroid/index.js.map +0 -1
- package/out/street/index.d.ts.map +0 -1
- package/out/street/index.js.map +0 -1
- package/out/uprn/index.d.ts.map +0 -1
- package/out/uprn/index.js.map +0 -1
- /package/lib/{geonames/index.ts → geonames.ts} +0 -0
- /package/lib/{nsul/index.ts → nsul.ts} +0 -0
- /package/lib/{poi/index.ts → poi.ts} +0 -0
- /package/lib/{postal/index.ts → postal.ts} +0 -0
- /package/lib/{uprn/index.ts → uprn.ts} +0 -0
package/lib/candidate/lookup.ts
CHANGED
|
@@ -2,35 +2,17 @@
|
|
|
2
2
|
* @copyright Sister Software
|
|
3
3
|
* @license AGPL-3.0
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
|
-
*
|
|
6
|
-
* Node-side {@link PlaceLookup} over the byte-range CANDIDATE table (`build-candidate.ts`) — the
|
|
7
|
-
* SAME gazetteer the browser runtime resolves against ({@link WOFCandidateTableLookup} in
|
|
8
|
-
* `packages/resolver-wof-wasm/lib/httpvfs/resolver.ts`), but reading a LOCAL `candidate.db` via
|
|
9
|
-
* `node:sqlite` instead of sql.js-httpvfs. This is what makes the server/CLI resolver match the
|
|
10
|
-
* browser: one lookup surface, one artifact, one ranking.
|
|
11
|
-
*
|
|
12
|
-
* The query is a single contiguous probe on the `WITHOUT ROWID` B-tree keyed `(name_key,
|
|
13
|
-
* country_id, region_id, placetype_id, neg_rank, spr_id)`. `name_key` is the SHARED
|
|
14
|
-
* {@link normalizeLocalityForKey} (build- and query-consistent), each row is denormalized (display
|
|
15
|
-
* `name`, centroid, bbox), and population rank is precomputed into `neg_rank` — so the result is
|
|
16
|
-
* POPULATION-FIRST and COUNTRY-AGNOSTIC (when no `country` filter is given), exactly like the
|
|
17
|
-
* demo. That's the deliberate divergence from {@link WOFSQLitePlaceLookup}'s FTS/bm25 ranking: a
|
|
18
|
-
* bare "Moscow" resolves to the 10.4 M-pop Russian city, not whichever same-name US township bm25
|
|
19
|
-
* floats to the top.
|
|
20
|
-
*
|
|
21
|
-
* Disambiguation rides the same mechanism the cascade already uses: a parsed region resolves to its
|
|
22
|
-
* stored bbox and the locality query is point-in-bbox-filtered on the candidate centroid (the
|
|
23
|
-
* `bbox` field on {@link FindPlaceQuery}).
|
|
24
5
|
*/
|
|
25
6
|
|
|
26
7
|
import { expandPlacetypeFilter } from "@mailwoman/codex/placetype-map"
|
|
8
|
+
import type { VariantAliasExemption } from "@mailwoman/core/geocode"
|
|
27
9
|
import { type Ancestor, type GazetteerArtifactCoverage, referentialFromPopulation } from "@mailwoman/core/resolver"
|
|
28
10
|
import { allRows } from "@mailwoman/core/utils"
|
|
29
11
|
import { jaroWinkler, levenshteinSimilarity } from "@mailwoman/match/comparators"
|
|
30
12
|
import { partitionByContainment } from "@mailwoman/resolver"
|
|
31
13
|
import { haversineKm } from "@mailwoman/spatial"
|
|
32
|
-
import { DatabaseClient } from "@mailwoman/sqlite/client"
|
|
33
|
-
import type
|
|
14
|
+
import type { DatabaseClient } from "@mailwoman/sqlite/client"
|
|
15
|
+
import { SQLiteLookup, type SQLiteLookupOptions } from "@mailwoman/sqlite/lookup"
|
|
34
16
|
|
|
35
17
|
import {
|
|
36
18
|
CANDIDATE_ANCESTOR_TABLE,
|
|
@@ -50,30 +32,22 @@ import { hasColumn, hasTable } from "#sqlite-utils"
|
|
|
50
32
|
import { type NameKey, normalizeLocalityForKey, stripLocalityQualifier } from "#street/normalize"
|
|
51
33
|
import type { FindPlaceQuery, PlaceCandidate, PlaceLookup, WOFPlacetype } from "#types"
|
|
52
34
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
* Path to a `candidate.db` built by `build-candidate.ts`. Opened read-only.
|
|
59
|
-
*/
|
|
60
|
-
databasePath?: PathBuilderLike
|
|
61
|
-
/**
|
|
62
|
-
* Pre-opened handle (tests / shared connections). Mutually exclusive with `databasePath`.
|
|
63
|
-
*/
|
|
64
|
-
database?: DatabaseClient<CandidateDatabase>
|
|
35
|
+
/**
|
|
36
|
+
* Where a {@link WOFCandidateTableLookup} reads from
|
|
37
|
+
* (a read-only `candidate.db` or a caller-held connection) and how it ranks.
|
|
38
|
+
*/
|
|
39
|
+
export interface WOFCandidateTableLookupOpts extends SQLiteLookupOptions<CandidateDatabase> {
|
|
65
40
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* without the role column.
|
|
41
|
+
* Exempt `name_role = 'variant'` aliases from the cross-country primary-preference penalty.
|
|
42
|
+
*
|
|
43
|
+
* - No-ops without the role column.
|
|
44
|
+
* - `"not_applied"` penalizes variant aliases like any other.
|
|
45
|
+
*
|
|
46
|
+
* @defaultValue `"applied"`.
|
|
69
47
|
*/
|
|
70
|
-
variantAliasExemption?:
|
|
48
|
+
variantAliasExemption?: VariantAliasExemption
|
|
71
49
|
}
|
|
72
50
|
|
|
73
|
-
/**
|
|
74
|
-
* The candidate columns this lookup probes — a typed projection of the SHARED {@link CandidateTable}, so a column rename
|
|
75
|
-
* in `build-candidate` (the writer) is a compile error here (the reader).
|
|
76
|
-
*/
|
|
77
51
|
type CandidateRow = Pick<
|
|
78
52
|
CandidateTable,
|
|
79
53
|
| "spr_id"
|
|
@@ -90,51 +64,19 @@ type CandidateRow = Pick<
|
|
|
90
64
|
| "is_primary"
|
|
91
65
|
| "population"
|
|
92
66
|
> &
|
|
93
|
-
//
|
|
94
|
-
// depends on the artifact: a candidate.db built before #28 has no such column and the probe leaves it
|
|
95
|
-
// out (see `#importanceSelect`). `undefined` therefore means "this build cannot tell you", which is the
|
|
96
|
-
// same answer as `null`'s "the score source had no measurement" — both are UNMEASURED, and the emit
|
|
97
|
-
// below collapses them into the one thing the consumer understands: no `importance` field at all.
|
|
67
|
+
// Optional because older artifacts may not include `importance`.
|
|
98
68
|
Partial<Pick<CandidateTable, "importance">>
|
|
99
69
|
|
|
100
|
-
/**
|
|
101
|
-
* FTS5-trigram over-fetch before the WORD-LEVEL re-rank. The trigram index stays the candidate GENERATOR (it is what
|
|
102
|
-
* the artifact carries); the scoring moved off trigram-Jaccard on 2026-08-12 (#1614), which the aucklnad receipts
|
|
103
|
-
* falsified as a typo measure: it scored the true transposition correction 'auckland' at 0.333 — below its own 0.34 bar
|
|
104
|
-
* — while 'auckley' scored 0.375 and the 'gore bay' scrape 0.455, because shared generic suffixes count as trigram
|
|
105
|
-
* evidence and transpositions count against it.
|
|
106
|
-
*/
|
|
107
70
|
const FUZZY_FETCH = 40
|
|
108
71
|
|
|
109
|
-
/**
|
|
110
|
-
* Minimum WORD-LEVEL similarity (max of Jaro-Winkler and normalized edit similarity — the `match/comparators`
|
|
111
|
-
* primitives, deliberately WITHOUT `nameSimilarity`'s token-subset floor, which is a person-name rule that would hand
|
|
112
|
-
* 'stanmore bay' to a place named 'Bay') for a fuzzy correction to count. Measured on the #1614 receipts:
|
|
113
|
-
* 'aucklnad'→'auckland' 0.975 (in), →'auckley' 0.868 (in, but outranked), 'stanmore bay'→'gore bay' ~0.70 (out),
|
|
114
|
-
* 'sacremento'→'sacramento' ~0.97 (in).
|
|
115
|
-
*/
|
|
116
72
|
const WORD_FUZZY_MIN = 0.85
|
|
117
73
|
|
|
118
|
-
/**
|
|
119
|
-
* The word-level correction similarity — see {@link WORD_FUZZY_MIN}.
|
|
120
|
-
*/
|
|
121
74
|
function wordFuzzySimilarity(a: string, b: string): number {
|
|
122
75
|
return Math.max(jaroWinkler(a, b), levenshteinSimilarity(a, b))
|
|
123
76
|
}
|
|
124
77
|
|
|
125
|
-
/**
|
|
126
|
-
* Postcode-containment re-rank check radius (km) — the SAME value the resolver's country pass measures at
|
|
127
|
-
* (`POSTCODE_COUNTRY_COHERENCE_THRESHOLD_KM`, resolver/postcode-country-coherence.ts): a locality within this distance
|
|
128
|
-
* of the postcode's own centroid counts as "containing" it. One number, two passes — a divergence here would make the
|
|
129
|
-
* two mechanisms disagree about what is proximal.
|
|
130
|
-
*/
|
|
131
78
|
const POSTCODE_CONTAINMENT_THRESHOLD_KM = 25
|
|
132
79
|
|
|
133
|
-
/**
|
|
134
|
-
* Unpadded character-trigrams of `s`, OR'd into an FTS5 trigram MATCH query (each quoted so FTS treats it as a literal
|
|
135
|
-
* term). Returns "" when `s` is shorter than a trigram or yields no clean grams — the caller then skips the fuzzy
|
|
136
|
-
* probe.
|
|
137
|
-
*/
|
|
138
80
|
function ftsTrigramQuery(s: string): string {
|
|
139
81
|
const grams = new Set<string>()
|
|
140
82
|
|
|
@@ -150,145 +92,79 @@ function ftsTrigramQuery(s: string): string {
|
|
|
150
92
|
}
|
|
151
93
|
|
|
152
94
|
/**
|
|
153
|
-
* Node {@link PlaceLookup} over `candidate.db`.
|
|
154
|
-
* — same `findPlace` contract, population-first ranking.
|
|
95
|
+
* Node {@link PlaceLookup} over `candidate.db`.
|
|
155
96
|
*/
|
|
156
|
-
export class WOFCandidateTableLookup implements PlaceLookup
|
|
157
|
-
#db: DatabaseClient<CandidateDatabase>
|
|
158
|
-
/**
|
|
159
|
-
* Resources this instance opened. A connection handed in by a caller is NOT in here, so disposal cannot reach it —
|
|
160
|
-
* ownership is membership rather than a flag a later branch has to check.
|
|
161
|
-
*/
|
|
162
|
-
readonly #resources = new DisposableStack()
|
|
97
|
+
export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> implements PlaceLookup {
|
|
163
98
|
readonly #countryToID = new Map<string, number>()
|
|
164
99
|
readonly #idToCountry = new Map<number, string>()
|
|
165
100
|
readonly #placetypeToID = new Map<string, number>()
|
|
166
101
|
readonly #idToPlacetype = new Map<number, string>()
|
|
167
|
-
|
|
168
|
-
* Prepared `(name_key, postcode)` probe for the #741 postal-city side-index — `undefined` when the
|
|
169
|
-
* `postal_city_candidate` table isn't present, so a candidate.db built without it is byte-stable.
|
|
170
|
-
*/
|
|
102
|
+
|
|
171
103
|
readonly #postalCityProbe: ReturnType<DatabaseClient["prepare"]> | undefined
|
|
172
|
-
|
|
173
|
-
* Prepared FTS5-trigram MATCH probe for the typo-tolerant fallback — `undefined` when the `candidate_fts` index isn't
|
|
174
|
-
* present, so a candidate.db built without it is byte-stable (the fuzzy path is skipped, exactly like today).
|
|
175
|
-
*/
|
|
104
|
+
|
|
176
105
|
readonly #ftsProbe: ReturnType<DatabaseClient["prepare"]> | undefined
|
|
177
106
|
/**
|
|
178
|
-
* Prepared
|
|
179
|
-
* fuzzy fallback: fuzzy is a TYPO corrector, so it engages only when the name doesn't exist in the gazetteer at all.
|
|
180
|
-
* A name that DOES exist but missed under the active filter is a filter miss (e.g. a placer misroute "Vienna,
|
|
181
|
-
* Austria"→IT), not a spelling miss — fuzzing it would scrape an unrelated same-country place and defeat the
|
|
182
|
-
* cascade's country-agnostic retry. Prepared only alongside `#ftsProbe`.
|
|
107
|
+
* Prepared unfiltered `name_key` existence probe, so fuzzy fallback runs only for true name misses.
|
|
183
108
|
*/
|
|
184
109
|
readonly #nameKeyExistsProbe: ReturnType<DatabaseClient["prepare"]> | undefined
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
* (the hard-country coverage check, guard-B plausibility) falls back to its code constants byte-identically.
|
|
189
|
-
*/
|
|
190
|
-
readonly artifactCoverage: GazetteerArtifactCoverage | undefined
|
|
191
|
-
/**
|
|
192
|
-
* `", importance"` when this artifact carries the #28 fame column, `""` when it does not — spliced into the probe's
|
|
193
|
-
* SELECT list. Existence-restricted exactly like `#ftsProbe` and `#postalCityProbe` above, and for the same reason: a
|
|
194
|
-
* candidate.db built before the column is a valid artifact, and naming a column it lacks would turn a stale gazetteer
|
|
195
|
-
* into `no such column` on the first keystroke rather than into "no fame signal", which is what it is.
|
|
196
|
-
*/
|
|
110
|
+
|
|
111
|
+
readonly artifactCoverage: GazetteerArtifactCoverage | null
|
|
112
|
+
|
|
197
113
|
readonly #importanceSelect: string
|
|
198
|
-
|
|
199
|
-
* Whether the artifact carries the #1730 `name_role` column — absent on pre-role builds, where the `excludeNameRoles`
|
|
200
|
-
* filter degrades to a no-op rather than erroring on a missing column.
|
|
201
|
-
*/
|
|
114
|
+
|
|
202
115
|
readonly #hasNameRole: boolean
|
|
203
116
|
readonly #variantAliasExemption: boolean
|
|
204
|
-
|
|
205
|
-
* `", name_role"` when the artifact carries the column — the probe SELECT rides it so the #1882 exemption can read
|
|
206
|
-
* the stamp off the row; empty on a pre-role build.
|
|
207
|
-
*/
|
|
117
|
+
|
|
208
118
|
readonly #roleSelect: string
|
|
209
|
-
|
|
210
|
-
* Prepared chain probe over the `candidate_ancestor` sidecar — `undefined` when the artifact predates it.
|
|
211
|
-
*/
|
|
119
|
+
|
|
212
120
|
readonly #ancestorsProbe: ReturnType<DatabaseClient["prepare"]> | undefined
|
|
213
121
|
readonly #ancestorsCache = new Map<number, Ancestor[]>()
|
|
214
|
-
|
|
215
|
-
* Prepared interval-label probe over `candidate_interval` — `undefined` when the artifact predates the sidecar, which
|
|
216
|
-
* is what makes the admin-containment re-rank (#1717 stage 2) capability-restricted: without it,
|
|
217
|
-
* `FindPlaceQuery.regionQualifier` is ignored, no candidate carries a `containedByQualifier` stamp, and the resolver
|
|
218
|
-
* walk reports the change `unavailable` instead of silently dead.
|
|
219
|
-
*/
|
|
122
|
+
|
|
220
123
|
readonly #intervalProbe: ReturnType<DatabaseClient["prepare"]> | undefined
|
|
221
124
|
readonly #intervalCache = new Map<number, IntervalLabel | null>()
|
|
222
|
-
|
|
223
|
-
* Prepared qualifier probe: the region-band rows (plus `country` — the region SLOT can hold a mislabeled country
|
|
224
|
-
* name, "Moscow, Russia" parses region="Russia") for one folded qualifier key. `undefined` when the artifact lacks
|
|
225
|
-
* the sidecar or the placetype dictionary lacks the band entirely.
|
|
226
|
-
*/
|
|
125
|
+
|
|
227
126
|
readonly #qualifierProbe: ReturnType<DatabaseClient["prepare"]> | undefined
|
|
228
127
|
/**
|
|
229
|
-
*
|
|
230
|
-
* order the FTS backend's `ancestorLineage` serves, read from the `candidate_ancestor` sidecar in one clustered
|
|
231
|
-
* probe. Backs `ResolveOpts.includeAncestors` (#404) on this backend, which is what puts region-class ancestry in
|
|
232
|
-
* front of the admin-coherence check (#1717).
|
|
233
|
-
*
|
|
234
|
-
* A PROPERTY, not a method, and assigned only when the artifact carries the sidecar: capability probes (`typeof
|
|
235
|
-
* backend.ancestors === "function"` — the resolver's gap report) then read the ARTIFACT truthfully. A candidate.db
|
|
236
|
-
* built before the sidecar reports the capability absent instead of presenting a method that answers `[]` for every
|
|
237
|
-
* place, which would be an absence dressed as a negative answer.
|
|
128
|
+
* Ancestor lineage accessor, nearest-first, present only when the artifact supports it.
|
|
238
129
|
*/
|
|
239
130
|
readonly ancestors: ((id: number | string) => Ancestor[]) | undefined
|
|
240
131
|
|
|
241
132
|
constructor(opts: WOFCandidateTableLookupOpts) {
|
|
242
|
-
|
|
243
|
-
this.#db = opts.database
|
|
244
|
-
} else if (opts.databasePath) {
|
|
245
|
-
this.#db = this.#resources.use(new DatabaseClient<CandidateDatabase>(opts.databasePath, { readOnly: true }))
|
|
246
|
-
} else {
|
|
247
|
-
throw new Error("WOFCandidateTableLookup needs `databasePath` or `database`")
|
|
248
|
-
}
|
|
133
|
+
super(opts)
|
|
249
134
|
|
|
250
|
-
|
|
251
|
-
// `findPlace` is a single B-tree probe with no dictionary round-trip.
|
|
252
|
-
for (const r of allRows<CountryCodeTable>(this.#db.prepare("SELECT id, code FROM country_codes"))) {
|
|
135
|
+
for (const r of allRows<CountryCodeTable>(this.database.prepare("SELECT id, code FROM country_codes"))) {
|
|
253
136
|
const code = String(r.code).toUpperCase()
|
|
254
137
|
this.#countryToID.set(code, Number(r.id))
|
|
255
138
|
this.#idToCountry.set(Number(r.id), code)
|
|
256
139
|
}
|
|
257
140
|
|
|
258
|
-
for (const r of allRows<PlacetypeCodeTable>(this
|
|
141
|
+
for (const r of allRows<PlacetypeCodeTable>(this.database.prepare("SELECT id, placetype FROM placetype_codes"))) {
|
|
259
142
|
this.#placetypeToID.set(String(r.placetype), Number(r.id))
|
|
260
143
|
this.#idToPlacetype.set(Number(r.id), String(r.placetype))
|
|
261
144
|
}
|
|
262
145
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (hasTable(this.#db, POSTAL_CITY_CANDIDATE_TABLE)) {
|
|
266
|
-
this.#postalCityProbe = this.#db.prepare(
|
|
146
|
+
if (hasTable(this.database, POSTAL_CITY_CANDIDATE_TABLE)) {
|
|
147
|
+
this.#postalCityProbe = this.database.prepare(
|
|
267
148
|
`SELECT spr_id, name, latitude, longitude FROM ${POSTAL_CITY_CANDIDATE_TABLE} WHERE name_key = ? AND postcode = ? LIMIT 1`
|
|
268
149
|
)
|
|
269
150
|
}
|
|
270
151
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (hasTable(this.#db, CANDIDATE_FTS_TABLE)) {
|
|
274
|
-
this.#ftsProbe = this.#db.prepare(
|
|
152
|
+
if (hasTable(this.database, CANDIDATE_FTS_TABLE)) {
|
|
153
|
+
this.#ftsProbe = this.database.prepare(
|
|
275
154
|
`SELECT name_key FROM ${CANDIDATE_FTS_TABLE} WHERE ${CANDIDATE_FTS_TABLE} MATCH ? ORDER BY bm25(${CANDIDATE_FTS_TABLE}) LIMIT ?`
|
|
276
155
|
)
|
|
277
156
|
|
|
278
|
-
this.#nameKeyExistsProbe = this
|
|
157
|
+
this.#nameKeyExistsProbe = this.database.prepare("SELECT 1 FROM candidate WHERE name_key = ? LIMIT 1")
|
|
279
158
|
}
|
|
280
159
|
|
|
281
|
-
//
|
|
282
|
-
this.#importanceSelect = hasColumn(this
|
|
283
|
-
this.#hasNameRole = hasColumn(this
|
|
284
|
-
this.#variantAliasExemption = opts.variantAliasExemption ===
|
|
160
|
+
// Optional columns are detected once here for hot-path reads.
|
|
161
|
+
this.#importanceSelect = hasColumn(this.database, "candidate", "importance") ? ", importance" : ""
|
|
162
|
+
this.#hasNameRole = hasColumn(this.database, "candidate", "name_role")
|
|
163
|
+
this.#variantAliasExemption = (opts.variantAliasExemption ?? "applied") === "applied"
|
|
285
164
|
this.#roleSelect = this.#hasNameRole ? ", name_role" : ""
|
|
286
165
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
// rather than as a method that answers [] everywhere.
|
|
290
|
-
if (hasTable(this.#db, CANDIDATE_ANCESTOR_TABLE)) {
|
|
291
|
-
this.#ancestorsProbe = this.#db.prepare(
|
|
166
|
+
if (hasTable(this.database, CANDIDATE_ANCESTOR_TABLE)) {
|
|
167
|
+
this.#ancestorsProbe = this.database.prepare(
|
|
292
168
|
`SELECT parent_spr_id, parent_placetype_id, parent_name FROM ${CANDIDATE_ANCESTOR_TABLE}` +
|
|
293
169
|
" WHERE spr_id = ? ORDER BY depth ASC"
|
|
294
170
|
)
|
|
@@ -296,34 +172,23 @@ export class WOFCandidateTableLookup implements PlaceLookup, Disposable {
|
|
|
296
172
|
this.ancestors = (id) => this.#ancestorLineage(id)
|
|
297
173
|
}
|
|
298
174
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
// truthfully) AND on the placetype dictionary carrying the qualifier band at all.
|
|
302
|
-
if (this.#ancestorsProbe && hasTable(this.#db, CANDIDATE_INTERVAL_TABLE)) {
|
|
303
|
-
this.#intervalProbe = this.#db.prepare(`SELECT pre, post FROM ${CANDIDATE_INTERVAL_TABLE} WHERE spr_id = ?`)
|
|
175
|
+
if (this.#ancestorsProbe && hasTable(this.database, CANDIDATE_INTERVAL_TABLE)) {
|
|
176
|
+
this.#intervalProbe = this.database.prepare(`SELECT pre, post FROM ${CANDIDATE_INTERVAL_TABLE} WHERE spr_id = ?`)
|
|
304
177
|
|
|
305
178
|
const bandIDs = [...REGION_CLASS_PLACETYPES, "country"]
|
|
306
179
|
.map((placetype) => this.#placetypeToID.get(placetype))
|
|
307
180
|
.filter((id): id is number => id !== undefined)
|
|
308
181
|
|
|
309
182
|
if (bandIDs.length) {
|
|
310
|
-
this.#qualifierProbe = this
|
|
183
|
+
this.#qualifierProbe = this.database.prepare(
|
|
311
184
|
`SELECT DISTINCT spr_id FROM candidate WHERE name_key = ? AND placetype_id IN (${bandIDs.join(",")}) LIMIT 8`
|
|
312
185
|
)
|
|
313
186
|
}
|
|
314
187
|
}
|
|
315
188
|
|
|
316
|
-
|
|
317
|
-
// the probes above — a candidate.db built before the manifest reads `undefined` and consumers keep
|
|
318
|
-
// their code-constant fallbacks byte-identically.
|
|
319
|
-
this.artifactCoverage = readGazetteerCoverageManifest(this.#db)
|
|
189
|
+
this.artifactCoverage = readGazetteerCoverageManifest(this.database)
|
|
320
190
|
}
|
|
321
191
|
|
|
322
|
-
/**
|
|
323
|
-
* The memoized chain read behind {@link ancestors}. Sync raw `.prepare()` on purpose — the backend contract's
|
|
324
|
-
* `ancestors()` is synchronous (the sync-by-interface resolver-reader rule), and the sidecar row already carries the
|
|
325
|
-
* parent's name and placetype, so this is one clustered probe with no join.
|
|
326
|
-
*/
|
|
327
192
|
#ancestorLineage(id: number | string): Ancestor[] {
|
|
328
193
|
const pid = typeof id === "number" ? id : Number(id)
|
|
329
194
|
|
|
@@ -349,10 +214,6 @@ export class WOFCandidateTableLookup implements PlaceLookup, Disposable {
|
|
|
349
214
|
return lineage
|
|
350
215
|
}
|
|
351
216
|
|
|
352
|
-
/**
|
|
353
|
-
* The interval label for one place, memoized. `null` is a real answer — the place has no recorded ancestry in the
|
|
354
|
-
* source (absence semantics: UNVERIFIABLE, never a containment verdict) — and is cached as such.
|
|
355
|
-
*/
|
|
356
217
|
#intervalLabel(sprID: number): IntervalLabel | null {
|
|
357
218
|
if (!this.#intervalProbe) return null
|
|
358
219
|
|
|
@@ -368,14 +229,7 @@ export class WOFCandidateTableLookup implements PlaceLookup, Disposable {
|
|
|
368
229
|
return label
|
|
369
230
|
}
|
|
370
231
|
|
|
371
|
-
|
|
372
|
-
* The qualifier's own rows in the candidate table: every region-band (+ country) place whose `name_key` matches one
|
|
373
|
-
* of the qualifier's {@link regionQualifierProbeKeys} expansions. Alias keys participate — `Thüringen` finds the row
|
|
374
|
-
* stored as `Thuringia` through the artifact's own alias keying, which is precisely the variant-form bridge the
|
|
375
|
-
* admin-coherence verdicts' fold-equality bound cannot offer (its stated v1 bound). Empty = the qualifier names
|
|
376
|
-
* nothing the artifact knows; the caller then stamps `false` everywhere and reorders nothing.
|
|
377
|
-
*/
|
|
378
|
-
#qualifierRegionIDs(qualifier: string, country: string | undefined): Set<number> {
|
|
232
|
+
#qualifierRegionIDs(qualifier: string, country: string | null): Set<number> {
|
|
379
233
|
const ids = new Set<number>()
|
|
380
234
|
|
|
381
235
|
if (!this.#qualifierProbe) return ids
|
|
@@ -392,10 +246,8 @@ export class WOFCandidateTableLookup implements PlaceLookup, Disposable {
|
|
|
392
246
|
}
|
|
393
247
|
|
|
394
248
|
/**
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
* a `false` there means "not contained along the canonical hierarchy", and the chain probe (one clustered read of
|
|
398
|
-
* ≤{@link MAX_ANCESTOR_DEPTH} rows) is the complete record that determines the result.
|
|
249
|
+
* Whether `sprID` is contained by any qualifier row, using interval labels first
|
|
250
|
+
* and the ancestor chain as fallback.
|
|
399
251
|
*/
|
|
400
252
|
#containedByQualifier(sprID: number, qualifierIDs: ReadonlySet<number>, qualifierLabels: IntervalLabel[]): boolean {
|
|
401
253
|
if (qualifierIDs.has(sprID)) return true
|
|
@@ -408,28 +260,13 @@ export class WOFCandidateTableLookup implements PlaceLookup, Disposable {
|
|
|
408
260
|
}
|
|
409
261
|
|
|
410
262
|
/**
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
* 1. Resolve the qualifier to its region-band rows ({@link #qualifierRegionIDs}) and stamp every existing row's
|
|
414
|
-
* `containedByQualifier` — the stamp is the trace surface, written even when nothing reorders.
|
|
415
|
-
* 2. INJECT contained same-key candidates the country scope hid: the deciding-site measurement (2026-08-18, the #1729
|
|
416
|
-
* lesson re-confirmed) showed `Weimar, Thüringen` under the en-US locale probes `country_id = US`, so the DE row
|
|
417
|
-
* is not IN the list and no reorder of the list can reach it. The injection probe runs the same exact fold (and,
|
|
418
|
-
* on a contained-miss, the qualifier-strip variant restricted to primary keys — the #1626 alias-scrape guard)
|
|
419
|
-
* under the SHAPE conds only, appends contained rows not already present, and never removes anything — recall can
|
|
420
|
-
* only widen. The typo-fuzzy tier is deliberately not probed: a qualifier cannot vouch for a name the gazetteer
|
|
421
|
-
* does not carry.
|
|
422
|
-
* 3. Partition contained-first — the SHARED {@link partitionByContainment} (tier-safe, stable; the resolver walk runs
|
|
423
|
-
* the same function after its fame re-rank, one function at both deciding sites per the #861 rule) — then
|
|
424
|
-
* re-window to `limit`.
|
|
425
|
-
*
|
|
426
|
-
* A qualifier that matches nothing stamps `false` everywhere and reorders nothing — byte-identical answers, and the
|
|
427
|
-
* walk's verdict reads `no_contained_candidate` rather than `unavailable` (the question WAS asked).
|
|
263
|
+
* Apply admin-containment re-rank: stamp containment, inject contained misses,
|
|
264
|
+
* then partition contained-first.
|
|
428
265
|
*/
|
|
429
266
|
#applyAdminContainment(
|
|
430
267
|
rows: Array<RankedRow<CandidateRow>>,
|
|
431
268
|
qualifier: string,
|
|
432
|
-
country: string |
|
|
269
|
+
country: string | null,
|
|
433
270
|
opts: {
|
|
434
271
|
nameKey: NameKey
|
|
435
272
|
strippedKey: NameKey
|
|
@@ -468,7 +305,7 @@ export class WOFCandidateTableLookup implements PlaceLookup, Disposable {
|
|
|
468
305
|
|
|
469
306
|
const injectFrom = (key: string, primaryOnly: boolean): void => {
|
|
470
307
|
const fetched = allRows<CandidateRow>(
|
|
471
|
-
this
|
|
308
|
+
this.database.prepare(injectSQL(primaryOnly)),
|
|
472
309
|
key,
|
|
473
310
|
...opts.shapeParams,
|
|
474
311
|
RERANK_FETCH
|
|
@@ -486,16 +323,7 @@ export class WOFCandidateTableLookup implements PlaceLookup, Disposable {
|
|
|
486
323
|
|
|
487
324
|
injectFrom(opts.nameKey, false)
|
|
488
325
|
|
|
489
|
-
//
|
|
490
|
-
// cannot reach a neighbourhood-tier namesake, so a CONTAINED one is structurally invisible no matter
|
|
491
|
-
// how the list reorders — the Astoria class: Queens' Astoria is a WOF neighbourhood, and the walk
|
|
492
|
-
// answered the Oregon locality under `qualifier="NY"` because nothing in the pool sat under NY. The
|
|
493
|
-
// widening is injection-only and triple-conditioned: the band is explicit (neighbourhood/macrohood/microhood
|
|
494
|
-
// — never region or country tiers), admission still requires the sidecar's containment proof, and only
|
|
495
|
-
// primary-keyed rows enter (an alias-keyed neighbourhood is the #1626 scrape class). Recall can only
|
|
496
|
-
// widen, and only toward rows the qualifier vouches for. `opts.shapeFilters`' bbox clause is
|
|
497
|
-
// deliberately not carried: containment is the stronger constraint, and the two co-occurring is not a
|
|
498
|
-
// measured shape.
|
|
326
|
+
// Also inject contained neighborhood-band namesakes (primary-keyed only).
|
|
499
327
|
const bandIDs = ["neighbourhood", "macrohood", "microhood"]
|
|
500
328
|
.map((placetype) => this.#placetypeToID.get(placetype))
|
|
501
329
|
.filter((id): id is number => id !== undefined)
|
|
@@ -506,7 +334,7 @@ export class WOFCandidateTableLookup implements PlaceLookup, Disposable {
|
|
|
506
334
|
`${this.#importanceSelect} FROM candidate WHERE name_key = ? AND placetype_id IN (${bandIDs.map(() => "?").join(",")}) AND is_primary = 1 ` +
|
|
507
335
|
"ORDER BY neg_rank ASC LIMIT ?"
|
|
508
336
|
|
|
509
|
-
const fetched = allRows<CandidateRow>(this
|
|
337
|
+
const fetched = allRows<CandidateRow>(this.database.prepare(bandSQL), opts.nameKey, ...bandIDs, RERANK_FETCH)
|
|
510
338
|
|
|
511
339
|
for (const row of fetched) {
|
|
512
340
|
const sprID = Number(row.spr_id)
|
|
@@ -518,8 +346,7 @@ export class WOFCandidateTableLookup implements PlaceLookup, Disposable {
|
|
|
518
346
|
}
|
|
519
347
|
}
|
|
520
348
|
|
|
521
|
-
//
|
|
522
|
-
// nothing, and primary-keyed only (a stripped surface never named an alias — #1626).
|
|
349
|
+
// Try stripped-key injection only when exact-key containment found no row.
|
|
523
350
|
if (
|
|
524
351
|
!injected.length &&
|
|
525
352
|
!rows.some((row) => row.containedByQualifier) &&
|
|
@@ -538,9 +365,6 @@ export class WOFCandidateTableLookup implements PlaceLookup, Disposable {
|
|
|
538
365
|
).slice(0, opts.limit)
|
|
539
366
|
}
|
|
540
367
|
|
|
541
|
-
/**
|
|
542
|
-
* Does this query want a locality-tier place? Postal-city aliases (#741) are all localities.
|
|
543
|
-
*/
|
|
544
368
|
#wantsLocality(placetype: FindPlaceQuery["placetype"]): boolean {
|
|
545
369
|
if (!placetype) return true
|
|
546
370
|
const want = Array.isArray(placetype) ? placetype : [placetype]
|
|
@@ -548,13 +372,6 @@ export class WOFCandidateTableLookup implements PlaceLookup, Disposable {
|
|
|
548
372
|
return expandPlacetypeFilter(want as readonly string[]).includes("locality")
|
|
549
373
|
}
|
|
550
374
|
|
|
551
|
-
/**
|
|
552
|
-
* The postcode-containment anchor: the postcode's own centroid row in the candidate table, keyed whitespace-stripped
|
|
553
|
-
* (#920 — the same fold the build applies to postcode rows), country-scoped when the query is, first
|
|
554
|
-
* coordinate-bearing row wins. null when the candidate table carries no such postcode — the re-rank then abstains,
|
|
555
|
-
* because a recall gap is not evidence for the name match. Meaning-of-zero: a 0,0 row is the build's unlocated
|
|
556
|
-
* sentinel, never a real centroid.
|
|
557
|
-
*/
|
|
558
375
|
#postcodeAnchor(postcode: string, country?: string): { lat: number; lon: number } | null {
|
|
559
376
|
const placetypeID = this.#placetypeToID.get("postalcode")
|
|
560
377
|
|
|
@@ -566,12 +383,12 @@ export class WOFCandidateTableLookup implements PlaceLookup, Disposable {
|
|
|
566
383
|
if (country) {
|
|
567
384
|
const countryID = this.#countryToID.get(country.toUpperCase())
|
|
568
385
|
|
|
569
|
-
if (countryID === undefined) return null //
|
|
386
|
+
if (countryID === undefined) return null // Unknown country in this artifact.
|
|
570
387
|
conds.push("country_id = ?")
|
|
571
388
|
params.push(countryID)
|
|
572
389
|
}
|
|
573
390
|
|
|
574
|
-
const row = this
|
|
391
|
+
const row = this.database
|
|
575
392
|
.prepare(`SELECT latitude, longitude FROM candidate WHERE ${conds.join(" AND ")} ORDER BY neg_rank ASC LIMIT 1`)
|
|
576
393
|
.get(...params) as { latitude: number; longitude: number } | undefined
|
|
577
394
|
|
|
@@ -585,9 +402,7 @@ export class WOFCandidateTableLookup implements PlaceLookup, Disposable {
|
|
|
585
402
|
|
|
586
403
|
if (!text) return []
|
|
587
404
|
|
|
588
|
-
//
|
|
589
|
-
// form at build (the GeoNames fold normalizes '624 66' → '62466'), so a postcode-typed query
|
|
590
|
-
// strips internal whitespace before keying. Postcode-only — locality names keep their spaces.
|
|
405
|
+
// Postcode queries use whitespace-stripped keying.
|
|
591
406
|
const wantsPostcode = [query.placetype].flat().includes("postalcode")
|
|
592
407
|
|
|
593
408
|
if (wantsPostcode) {
|
|
@@ -598,12 +413,6 @@ export class WOFCandidateTableLookup implements PlaceLookup, Disposable {
|
|
|
598
413
|
|
|
599
414
|
if (!nameKey) return []
|
|
600
415
|
|
|
601
|
-
// #741: postcode-keyed postal-city alias. An exact `(name_key, postcode)` hit resolves a
|
|
602
|
-
// user-typed POSTAL city ("Antioch", 37013) to the geographic locality the postcode sits in
|
|
603
|
-
// ("Nashville"), bypassing the population/region ranking that can't see the postcode. Conditioned on
|
|
604
|
-
// the side-index being present, a postcode in the query, and a locality-tier request — so the
|
|
605
|
-
// common (no-postcode / non-locality) path is untouched. A hit short-circuits: the postcode is
|
|
606
|
-
// an exact, high-confidence disambiguator, so we return the single geographic locality.
|
|
607
416
|
if (query.postcode && this.#postalCityProbe && this.#wantsLocality(query.placetype)) {
|
|
608
417
|
const hit = this.#postalCityProbe.get(nameKey, query.postcode.trim()) as
|
|
609
418
|
| Pick<PostalCityCandidateTable, "spr_id" | "name" | "latitude" | "longitude">
|
|
@@ -618,8 +427,20 @@ export class WOFCandidateTableLookup implements PlaceLookup, Disposable {
|
|
|
618
427
|
country: query.country?.toUpperCase() ?? "",
|
|
619
428
|
lat: Number(hit.latitude),
|
|
620
429
|
lon: Number(hit.longitude),
|
|
430
|
+
parent_id: null,
|
|
621
431
|
score: 1,
|
|
432
|
+
distanceKm: null,
|
|
622
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,
|
|
623
444
|
},
|
|
624
445
|
]
|
|
625
446
|
}
|
|
@@ -627,11 +448,7 @@ export class WOFCandidateTableLookup implements PlaceLookup, Disposable {
|
|
|
627
448
|
|
|
628
449
|
const limit = Math.max(1, query.limit ?? 10)
|
|
629
450
|
|
|
630
|
-
//
|
|
631
|
-
// SHAPE subset (placetype/bbox/primary — everything but the country scope) is kept separately
|
|
632
|
-
// because the admin-containment injection probe (#1717 stage 2) runs under the shape conds
|
|
633
|
-
// WITHOUT the country: bypassing a locale-inferred country scope for a qualifier-vouched
|
|
634
|
-
// candidate is the change's whole point and the one filter injection may cross.
|
|
451
|
+
// Shape filters are tracked separately from the shared country scope for exact and stripped probes.
|
|
635
452
|
const filters: string[] = []
|
|
636
453
|
const filterParams: Array<string | number> = []
|
|
637
454
|
const shapeFilters: string[] = []
|
|
@@ -640,14 +457,13 @@ export class WOFCandidateTableLookup implements PlaceLookup, Disposable {
|
|
|
640
457
|
if (query.country) {
|
|
641
458
|
const cid = this.#countryToID.get(query.country.toUpperCase())
|
|
642
459
|
|
|
643
|
-
if (cid === undefined) return [] //
|
|
460
|
+
if (cid === undefined) return [] // Unknown country in this artifact.
|
|
644
461
|
filters.push("country_id = ?")
|
|
645
462
|
filterParams.push(cid)
|
|
646
463
|
}
|
|
647
464
|
|
|
648
465
|
if (query.placetype) {
|
|
649
|
-
//
|
|
650
|
-
// localadmin). `postalcode` maps to no admin placetype here → empty → no rows.
|
|
466
|
+
// Expand placetype equivalents (e.g. locality includes borough/localadmin).
|
|
651
467
|
const want = Array.isArray(query.placetype) ? query.placetype : [query.placetype]
|
|
652
468
|
|
|
653
469
|
const ids = expandPlacetypeFilter(want as readonly string[])
|
|
@@ -665,116 +481,60 @@ export class WOFCandidateTableLookup implements PlaceLookup, Disposable {
|
|
|
665
481
|
shapeParams.push(b.minLat, b.maxLat, b.minLon, b.maxLon)
|
|
666
482
|
}
|
|
667
483
|
|
|
668
|
-
//
|
|
669
|
-
// is a token taken out of a longer classified span never NAMED an alias, so alias-keyed rows must not
|
|
670
|
-
// answer it — 'Savile Row''s token 'Row' resolved Rhu, Scotland (585 km) through the village's
|
|
671
|
-
// historical-name alias key. Whole-input bare probes never set this, keeping the exonym recall the
|
|
672
|
-
// #1546 note protects (Москва's alias rows answer 'Moscow').
|
|
484
|
+
// Optional guard: restrict to primary rows for token-extracted probes.
|
|
673
485
|
if (query.primaryOnly) {
|
|
674
486
|
shapeFilters.push("is_primary = 1")
|
|
675
487
|
}
|
|
676
488
|
|
|
677
|
-
// The role
|
|
678
|
-
// role-NULL exonym tier open — the distinction `primaryOnly` cannot express. Degrades to a no-op
|
|
679
|
-
// on an artifact without the column.
|
|
489
|
+
// The role filter is skipped on an artifact lacking `name_role`.
|
|
680
490
|
if (query.excludeNameRoles?.length && this.#hasNameRole) {
|
|
681
491
|
shapeFilters.push(`(name_role IS NULL OR name_role NOT IN (${query.excludeNameRoles.map(() => "?").join(",")}))`)
|
|
682
492
|
shapeParams.push(...query.excludeNameRoles)
|
|
683
493
|
}
|
|
684
494
|
|
|
685
|
-
// The main-probe conds are country-then-shape, exactly the order they have always been.
|
|
686
495
|
filters.push(...shapeFilters)
|
|
687
496
|
filterParams.push(...shapeParams)
|
|
688
497
|
|
|
689
|
-
// Region scope
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
// population-first and "Springfield, IL" (parentID = Illinois) drops to the larger Springfield, MO.
|
|
694
|
-
// Kept OUT of the shared `filters` so a region MISS falls back to the unscoped cascade below: a
|
|
695
|
-
// country/non-region parent (no `region_id` match), a `region_id=0` row (place with no region
|
|
696
|
-
// ancestor), or a wrong parent degrades to today's behavior — never worse, recall-safe by construction.
|
|
697
|
-
const regionParentID = query.parentID || undefined
|
|
698
|
-
|
|
699
|
-
const probe = (nk: string, regionID: number | undefined, countryID?: number): Array<RankedRow<CandidateRow>> => {
|
|
498
|
+
// Region scope is optional (`region_id = parentID`); the fallback stays unscoped.
|
|
499
|
+
const regionParentID = query.parentID || null
|
|
500
|
+
|
|
501
|
+
const probe = (nk: string, regionID: number | null, countryID?: number): Array<RankedRow<CandidateRow>> => {
|
|
700
502
|
const conds = ["name_key = ?", ...filters]
|
|
701
503
|
const params: Array<string | number> = [nk, ...filterParams]
|
|
702
504
|
|
|
703
|
-
if (regionID !==
|
|
505
|
+
if (regionID !== null) {
|
|
704
506
|
conds.push("region_id = ?")
|
|
705
507
|
params.push(regionID)
|
|
706
508
|
}
|
|
707
509
|
|
|
708
|
-
// #1585: the fuzzy tier's country scope — only the corrected-key probes pass this.
|
|
709
510
|
if (typeof countryID === "number") {
|
|
710
511
|
conds.push("country_id = ?")
|
|
711
512
|
params.push(countryID)
|
|
712
513
|
}
|
|
713
514
|
|
|
714
|
-
//
|
|
715
|
-
// RERANK_FETCH so the bounded cross-country primary-preference re-rank (below) can promote the
|
|
716
|
-
// intended primary even when a cluster of more-populous foreign aliases sits ahead of it. `is_primary`
|
|
717
|
-
// + `country_id` feed that re-rank. A single-country probe (a country filter, or all rows same
|
|
718
|
-
// country) re-ranks to the identical population order, so the common path is untouched.
|
|
719
|
-
// `population` rides along for the REFERENTIAL score on the result (ROAD_TO_V9 §2) — one more
|
|
720
|
-
// column off a clustered row the probe already reads, and it is NOT what the probe orders by:
|
|
721
|
-
// `neg_rank` remains the sort key, so this changes no ordering, only what the result reports.
|
|
722
|
-
// `importance` (#28) rides along on the same terms, and there is deliberately NO `ORDER BY` on it:
|
|
723
|
-
// the fame prior is applied by the RESOLVER (`resolver/toponym-prior.ts`), which alone knows
|
|
724
|
-
// whether the query was bare enough to deserve it. A backend that pre-sorted by fame would apply
|
|
725
|
-
// it to every lookup, including the qualified addresses the D-rule guard exists to protect.
|
|
515
|
+
// Population-ordered fetch over-fetches for the bounded primary-preference rerank.
|
|
726
516
|
const sql =
|
|
727
517
|
"SELECT spr_id, name, country_id, placetype_id, latitude, longitude, min_lat, min_lon, max_lat, max_lon, neg_rank, is_primary, population" +
|
|
728
518
|
`${this.#importanceSelect}${this.#roleSelect} FROM candidate WHERE ${conds.join(" AND ")} ORDER BY neg_rank ASC LIMIT ?`
|
|
729
519
|
|
|
730
|
-
const fetched = allRows<CandidateRow>(this
|
|
520
|
+
const fetched = allRows<CandidateRow>(this.database.prepare(sql), ...params, Math.max(limit, RERANK_FETCH))
|
|
731
521
|
|
|
732
522
|
return rankByPrimaryPreference(fetched, limit, undefined, this.#idToPlacetype, this.#variantAliasExemption)
|
|
733
523
|
}
|
|
734
524
|
|
|
735
|
-
|
|
736
|
-
// only tightens an already-population-first pick, so a region MISS re-runs the whole cascade unscoped
|
|
737
|
-
// (below) rather than dropping a place that has no in-region row.
|
|
738
|
-
const cascade = (regionID: number | undefined): Array<RankedRow<CandidateRow>> => {
|
|
525
|
+
const cascade = (regionID: number | null): Array<RankedRow<CandidateRow>> => {
|
|
739
526
|
let rows = probe(nameKey, regionID)
|
|
740
527
|
|
|
741
528
|
if (!rows.length) {
|
|
742
|
-
// Query-side qualifier-strip fallback: an OA locality with a qualifier the gazetteer's
|
|
743
|
-
// canonical name omits ("Lenk im Simmental" → "Lenk", "Roche VD"). Tried ONLY on an exact
|
|
744
|
-
// miss; the cascade's region bbox disambiguates any base-name ambiguity.
|
|
745
529
|
const strippedKey = normalizeLocalityForKey(stripLocalityQualifier(text))
|
|
746
530
|
|
|
747
531
|
if (strippedKey && strippedKey !== nameKey) {
|
|
748
|
-
//
|
|
749
|
-
// scrape, not a qualifier match — 'Savile Row' stripped to 'row' resolved Rhu, Scotland
|
|
750
|
-
// (585 km) through the village's historical-name alias. The legitimate qualifier class
|
|
751
|
-
// matches the place's own primary key ('Lenk im Simmental' → the Lenk row keyed 'lenk',
|
|
752
|
-
// is_primary=1), so refusing alias-keyed rows keeps every intended case and kills the
|
|
753
|
-
// scrape. The alias tier remains fully available to EXACT queries — only the stripped
|
|
754
|
-
// RETRY loses it, because the query's own surface never named the alias.
|
|
532
|
+
// Stripped probes only accept primary rows to avoid alias-scrape matches.
|
|
755
533
|
rows = probe(strippedKey, regionID).filter((r) => r.is_primary === 1)
|
|
756
534
|
}
|
|
757
535
|
}
|
|
758
536
|
|
|
759
|
-
// Typo-tolerant fallback
|
|
760
|
-
// misspelling the normalized key can't reach. FTS5-trigram fetches a loose set; we re-rank by
|
|
761
|
-
// trigram-Jaccard (the admin backend's measure) and probe the best name_keys, so a typo resolves
|
|
762
|
-
// the same on either backend. The country/placetype/bbox/region filters still apply via `probe`.
|
|
763
|
-
// Skipped when the index is absent (byte-stable for an older candidate.db).
|
|
764
|
-
//
|
|
765
|
-
// Condition: only when the name doesn't exist in the gazetteer AT ALL (unfiltered). A name that exists
|
|
766
|
-
// but missed under the active country/placetype/bbox filter is a FILTER miss, not a spelling miss
|
|
767
|
-
// — fuzzing it scrapes an unrelated same-filter place ("Vienna, Austria" misrouted to IT would
|
|
768
|
-
// pull a tiny Italian name_key near Siena) and masks the cascade's country-agnostic retry that
|
|
769
|
-
// correctly lands population-first Vienna AT. The exact/strip probes already covered the real name.
|
|
770
|
-
//
|
|
771
|
-
// NEVER for postcodes: fuzzy is a typo corrector for place NAMES, and a "corrected" postcode is a
|
|
772
|
-
// DIFFERENT postcode. The 2026-08-05 Code-Point swap exposed the trap at scale: Northern Ireland's
|
|
773
|
-
// `BT3 9QQ` (absent — no permissive NI source) trigram-matched Sheffield's `S3 9QQ` (Jaccard 0.4
|
|
774
|
-
// on {39q, 9qq}) and resolved 200+ km wrong with full confidence. An unknown postcode must abstain.
|
|
775
|
-
// #1585: a locale HINT scopes the typo tier to its country. Only when no hard `country`
|
|
776
|
-
// filter is active (that is already narrower); a scope naming a country the table doesn't
|
|
777
|
-
// carry is a SCOPED-EMPTY — the fuzzy tier abstains rather than falling through worldwide.
|
|
537
|
+
// Typo-tolerant fallback for true name misses only, skipping postcodes and respecting the fuzzy-country scope.
|
|
778
538
|
const fuzzyCountryID =
|
|
779
539
|
!query.country && query.fuzzyCountry ? this.#countryToID.get(query.fuzzyCountry.toUpperCase()) : undefined
|
|
780
540
|
|
|
@@ -796,7 +556,7 @@ export class WOFCandidateTableLookup implements PlaceLookup, Disposable {
|
|
|
796
556
|
const ranked = hits
|
|
797
557
|
.map((h) => ({ nk: String(h.name_key), s: wordFuzzySimilarity(nameKey, String(h.name_key)) }))
|
|
798
558
|
.filter((h) => h.s >= WORD_FUZZY_MIN)
|
|
799
|
-
// oxlint-disable-next-line unicorn/no-array-sort -- sorts a freshly-built array
|
|
559
|
+
// oxlint-disable-next-line unicorn/no-array-sort -- sorts a freshly-built array. toSorted would double-allocate on a hot path
|
|
800
560
|
.sort((a, b) => b.s - a.s)
|
|
801
561
|
|
|
802
562
|
const seen = new Set<string>()
|
|
@@ -804,8 +564,7 @@ export class WOFCandidateTableLookup implements PlaceLookup, Disposable {
|
|
|
804
564
|
for (const h of ranked) {
|
|
805
565
|
if (seen.has(h.nk)) continue
|
|
806
566
|
seen.add(h.nk)
|
|
807
|
-
|
|
808
|
-
// fuzzy matches and `exactMatch` below must say so — see `RankedRow.fuzzy`.
|
|
567
|
+
|
|
809
568
|
rows.push(...probe(h.nk, regionID, fuzzyCountryID).map((r) => ({ ...r, fuzzy: true })))
|
|
810
569
|
|
|
811
570
|
if (rows.length >= limit) break
|
|
@@ -819,27 +578,14 @@ export class WOFCandidateTableLookup implements PlaceLookup, Disposable {
|
|
|
819
578
|
}
|
|
820
579
|
|
|
821
580
|
let rows = cascade(regionParentID)
|
|
822
|
-
|
|
823
|
-
// interior check the resolver-side trace (#1721) cannot otherwise see. Stamped onto every returned
|
|
824
|
-
// place, because the re-admission path is exactly where a wrong-instance namesake enters.
|
|
581
|
+
|
|
825
582
|
let regionScopeMiss = false
|
|
826
583
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
// resolves exactly as it does today. Only when a region scope was actually applied.
|
|
830
|
-
if (!rows.length && regionParentID !== undefined) {
|
|
831
|
-
rows = cascade(undefined)
|
|
584
|
+
if (!rows.length && regionParentID !== null) {
|
|
585
|
+
rows = cascade(null)
|
|
832
586
|
regionScopeMiss = rows.length > 0
|
|
833
587
|
}
|
|
834
588
|
|
|
835
|
-
// Postcode-containment coherence (#31, Mechanism 2): re-rank the rows by proximity to the postcode's
|
|
836
|
-
// own centroid, so the locality that CONTAINS the postcode wins the name-match tie (the "Paris" that
|
|
837
|
-
// holds 75001, not the one that holds a 75001-free namesake). The resolver sends this flag on locality
|
|
838
|
-
// lookups when `ResolveOpts.postcodeContainmentCoherence` is on. Strictly beneath the #741 postal-city
|
|
839
|
-
// short-circuit above — an exact (name, postcode) hit IS the answer and outranks any re-rank — and after
|
|
840
|
-
// the region-scope fallback, so it sees the final row set. Rows within the radius sort by distance first;
|
|
841
|
-
// the out-of-radius tail keeps its original population-first order. No in-radius row, or no postcode row in
|
|
842
|
-
// the candidate table → unchanged (byte-identical to the flag-off path).
|
|
843
589
|
if (
|
|
844
590
|
query.postcode &&
|
|
845
591
|
query.postcodeContainmentCoherence === true &&
|
|
@@ -863,20 +609,16 @@ export class WOFCandidateTableLookup implements PlaceLookup, Disposable {
|
|
|
863
609
|
}
|
|
864
610
|
|
|
865
611
|
if (insideThreshold.length) {
|
|
866
|
-
// oxlint-disable-next-line unicorn/no-array-sort --
|
|
612
|
+
// oxlint-disable-next-line unicorn/no-array-sort -- hot-path in-place sort
|
|
867
613
|
insideThreshold.sort((a, b) => a.distanceKm - b.distanceKm)
|
|
868
614
|
rows = [...insideThreshold.map(({ row }) => row), ...outsideThreshold]
|
|
869
615
|
}
|
|
870
616
|
}
|
|
871
617
|
}
|
|
872
618
|
|
|
873
|
-
// Admin-containment re-rank
|
|
874
|
-
// address's outermost explicit statement, so its partition outranks the postcode-proximity order
|
|
875
|
-
// above (contained rows keep that order among themselves). Capability-conditioned on the sidecar
|
|
876
|
-
// (`#qualifierProbe`); when the artifact predates it, `regionQualifier` is ignored, no stamp is
|
|
877
|
-
// written, and the resolver walk reports the change `unavailable`.
|
|
619
|
+
// Admin-containment re-rank runs last.
|
|
878
620
|
if (query.regionQualifier?.trim() && this.#qualifierProbe && this.#wantsLocality(query.placetype)) {
|
|
879
|
-
rows = this.#applyAdminContainment(rows, query.regionQualifier.trim(), query.country, {
|
|
621
|
+
rows = this.#applyAdminContainment(rows, query.regionQualifier.trim(), query.country ?? null, {
|
|
880
622
|
nameKey,
|
|
881
623
|
strippedKey: normalizeLocalityForKey(stripLocalityQualifier(text)),
|
|
882
624
|
shapeFilters,
|
|
@@ -887,87 +629,54 @@ export class WOFCandidateTableLookup implements PlaceLookup, Disposable {
|
|
|
887
629
|
|
|
888
630
|
const candidates = rows.map((row): PlaceCandidate => {
|
|
889
631
|
const hasBbox = row.min_lat != null && row.max_lat != null && row.min_lon != null && row.max_lon != null
|
|
632
|
+
const parent = this.#ancestorLineage(Number(row.spr_id))[0]
|
|
633
|
+
const hasPopulation = row.population !== null && row.population > 0
|
|
890
634
|
|
|
891
635
|
return {
|
|
892
636
|
id: Number(row.spr_id),
|
|
893
637
|
name: String(row.name ?? ""),
|
|
894
638
|
placetype: (this.#idToPlacetype.get(Number(row.placetype_id)) ?? "") as WOFPlacetype,
|
|
895
|
-
|
|
896
|
-
// international postcode like 10115 = Berlin DE AND New York US must not out-resolve the city).
|
|
639
|
+
|
|
897
640
|
country: this.#idToCountry.get(Number(row.country_id)) ?? "",
|
|
898
641
|
lat: Number(row.latitude),
|
|
899
642
|
lon: Number(row.longitude),
|
|
900
|
-
|
|
901
|
-
|
|
643
|
+
|
|
644
|
+
parent_id: parent ? Number(parent.id) : null,
|
|
645
|
+
|
|
902
646
|
score: -Number(row.neg_rank),
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
// re-rank actually stick through resolution — without it the walk re-sorts by raw `score` and a
|
|
906
|
-
// more-populous foreign alias (Changchun for "Cancun") wins back the node. Equals `score` for every
|
|
907
|
-
// un-penalized row (primaries + same-country aliases), so the common ordering is unchanged.
|
|
647
|
+
distanceKm: null,
|
|
648
|
+
|
|
908
649
|
prominence: -Number(row.effectiveNegRank),
|
|
909
|
-
|
|
910
|
-
// accepts alias-exact hits ("New York City" → New York) the same as canonical — EXCEPT a cross-country
|
|
911
|
-
// alias that lost the bounded contest to a same-key primary (`demoted`): it drops to the partial tier so
|
|
912
|
-
// the walk's country posterior can't cross back over the primary (see `RankedRow.demoted`) — and
|
|
913
|
-
// EXCEPT a row the typo-corrector produced (`fuzzy`), which by definition answers a name the
|
|
914
|
-
// gazetteer does not carry (see `RankedRow.fuzzy`).
|
|
650
|
+
|
|
915
651
|
exactMatch: !row.demoted && !row.fuzzy,
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
...(row.population === null || row.population <= 0
|
|
930
|
-
? {}
|
|
931
|
-
: { population: row.population, referential: referentialFromPopulation(row.population) }),
|
|
932
|
-
// #28: the fame prior, from the `importance` column the candidate build joins in. Emitted ONLY
|
|
933
|
-
// when the artifact measured this place — an absent field is what `rankByImportance` reads as
|
|
934
|
-
// "does not participate", and a 0 would be a claim nobody made (meaning-of-zero).
|
|
935
|
-
//
|
|
936
|
-
// The field name matches the column because they hold the same thing: the score source's
|
|
937
|
-
// BLENDED prior — the concordance's encyclopedia-derived channel where a concordance matched, a
|
|
938
|
-
// population-derived proxy everywhere else. It is NOT the strict `encyclopedic` channel
|
|
939
|
-
// `place-importance-schema.ts` defines, and it deliberately does not land in that field: the
|
|
940
|
-
// strict channel was measured on 2026-08-10 and covers eleven countries, none of them CA/AU/RU,
|
|
941
|
-
// which makes it inert on three of the four homonym contests the prior exists to settle.
|
|
942
|
-
// `PlaceCandidate.encyclopedic` stays reserved for a strict-channel source (the FTS backend's
|
|
943
|
-
// clauses are strict and today emit NULL for everything — no shipped admin DB has the split
|
|
944
|
-
// table at all). See `candidate-schema.ts` → {@link CandidateTable.importance}.
|
|
945
|
-
...(typeof row.importance === "number" && Number.isFinite(row.importance)
|
|
946
|
-
? { importance: row.importance }
|
|
947
|
-
: {}),
|
|
948
|
-
...(hasBbox
|
|
652
|
+
|
|
653
|
+
regionScopeMiss: regionScopeMiss ? true : null,
|
|
654
|
+
|
|
655
|
+
containedByQualifier: row.containedByQualifier ?? null,
|
|
656
|
+
|
|
657
|
+
variantAliasExempted: row.variantExempted ? true : null,
|
|
658
|
+
|
|
659
|
+
population: hasPopulation ? row.population : null,
|
|
660
|
+
referential: hasPopulation ? referentialFromPopulation(row.population) : null,
|
|
661
|
+
encyclopedic: null,
|
|
662
|
+
|
|
663
|
+
importance: typeof row.importance === "number" && Number.isFinite(row.importance) ? row.importance : null,
|
|
664
|
+
bbox: hasBbox
|
|
949
665
|
? {
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
maxLon: Number(row.max_lon),
|
|
955
|
-
},
|
|
666
|
+
minLat: Number(row.min_lat),
|
|
667
|
+
maxLat: Number(row.max_lat),
|
|
668
|
+
minLon: Number(row.min_lon),
|
|
669
|
+
maxLon: Number(row.max_lon),
|
|
956
670
|
}
|
|
957
|
-
:
|
|
671
|
+
: null,
|
|
672
|
+
mismatch: null,
|
|
958
673
|
}
|
|
959
674
|
})
|
|
960
675
|
|
|
961
|
-
// Proximity re-rank (#938) — the shared implementation, so the browser byte-range twin runs the same
|
|
962
|
-
// code rather than the same constants. See proximity-rerank.ts for why that distinction mattered.
|
|
963
676
|
if (query.bias && query.bias.length) {
|
|
964
677
|
applyProximityRerank(candidates, query.bias)
|
|
965
678
|
}
|
|
966
679
|
|
|
967
680
|
return candidates
|
|
968
681
|
}
|
|
969
|
-
|
|
970
|
-
[Symbol.dispose](): void {
|
|
971
|
-
this.#resources[Symbol.dispose]()
|
|
972
|
-
}
|
|
973
682
|
}
|