@mailwoman/resolver-wof-sqlite 9.4.0 → 10.1.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 +8 -8
- package/lib/address/{point-interpolation.ts → point/interpolation.ts} +40 -38
- package/lib/address/point/schema.ts +132 -0
- package/lib/address/point.ts +68 -81
- package/lib/{address/index.ts → address.ts} +2 -2
- package/lib/ancestry/backfill.ts +24 -88
- package/lib/{ancestry/index.ts → ancestry.ts} +19 -15
- package/lib/build/candidate.ts +608 -0
- package/lib/build/population-corrections.ts +64 -0
- package/lib/build/slim.ts +338 -0
- package/lib/candidate/alias-bags.ts +74 -14
- package/lib/candidate/ancestors/schema.ts +211 -0
- package/lib/candidate/{ancestors-sidecar.ts → ancestors/sidecar.ts} +17 -24
- package/lib/candidate/country-display-names.ts +6 -20
- package/lib/candidate/extract-fold.ts +12 -57
- package/lib/candidate/fts.ts +28 -0
- package/lib/candidate/importance.ts +221 -0
- package/lib/candidate/lookup.ts +669 -0
- package/lib/candidate/name-roles.ts +16 -34
- package/lib/candidate/own-name.ts +34 -31
- package/lib/candidate/place-attrs.ts +20 -13
- package/lib/candidate/schema.ts +211 -0
- package/lib/candidate/scoring.ts +222 -0
- package/lib/capital-schema.ts +21 -17
- package/lib/capitals.ts +26 -24
- package/lib/coincident-roles.ts +74 -55
- package/lib/convention/schema.ts +13 -7
- package/lib/convention.ts +144 -0
- package/lib/coverage-manifest-schema.ts +34 -39
- package/lib/currency-backfill.ts +24 -69
- package/lib/env.ts +1 -1
- package/lib/exact-match.ts +31 -20
- package/lib/extracts.ts +96 -73
- package/lib/fst/autocomplete.ts +52 -35
- package/lib/fst/builder.ts +30 -36
- package/lib/fst/deserialize-web.ts +8 -9
- package/lib/fst/format.ts +41 -23
- package/lib/fst/freshness.ts +17 -95
- package/lib/fst/matcher.ts +2 -2
- package/lib/fst/serialize.ts +45 -60
- package/lib/fst/types.ts +71 -59
- package/lib/fts/query.ts +5 -36
- package/lib/fts.ts +280 -0
- package/lib/geonames/aliases.ts +72 -149
- package/lib/geonames/postal.ts +162 -77
- package/lib/index.ts +20 -27
- package/lib/interpolation.ts +43 -96
- package/lib/lookup.ts +137 -257
- package/lib/name-score.ts +9 -7
- package/lib/nsul/lookup.ts +36 -53
- package/lib/nsul/schema.ts +16 -43
- package/lib/paths.ts +159 -0
- package/lib/place-importance-schema.ts +57 -164
- package/lib/poi/lookup.ts +100 -109
- package/lib/poi/schema.ts +42 -26
- package/lib/polygon-schema.ts +10 -6
- package/lib/postal/city/alias/lookup.ts +79 -0
- package/lib/postal/{city-alias-schema.ts → city/alias/schema.ts} +14 -10
- package/lib/postal/{city-candidate-schema.ts → city/candidate-schema.ts} +26 -20
- package/lib/postal.ts +9 -0
- package/lib/postcode-point-lookup.ts +9 -19
- package/lib/primary-preference.ts +45 -125
- package/lib/proximity-rerank.ts +43 -32
- package/lib/ranking-weights.ts +98 -76
- package/lib/region-keys.ts +68 -40
- package/lib/reverse.ts +108 -72
- package/lib/schema.ts +74 -42
- package/lib/search-fetch.ts +90 -68
- package/lib/sqlite/convention-source.ts +57 -0
- package/lib/sqlite/utils.ts +79 -0
- package/lib/street/centroid/schema.ts +149 -0
- package/lib/street/centroid.ts +16 -25
- package/lib/street/{morphology-fst-builder.ts → morphology/fst/builder.ts} +56 -41
- package/lib/street/{morphology-fst-loader.ts → morphology/fst/loader.ts} +36 -29
- package/lib/street/name-lookup.ts +43 -35
- package/lib/street/normalize.ts +85 -216
- package/lib/street/segment-schema.ts +116 -43
- package/lib/{street/index.ts → street.ts} +3 -3
- package/lib/types.ts +94 -140
- package/lib/unified-schema.ts +29 -29
- package/lib/uprn/existence.ts +30 -19
- package/lib/uprn/lookup.ts +28 -67
- package/lib/uprn/schema.ts +18 -40
- package/lib/weights-overlay-linker.ts +204 -232
- package/out/address/point/interpolation.d.ts +45 -0
- package/out/address/point/interpolation.d.ts.map +1 -0
- package/out/address/{point-interpolation.js → point/interpolation.js} +38 -38
- package/out/address/point/interpolation.js.map +1 -0
- package/out/address/point/schema.d.ts +74 -0
- package/out/address/point/schema.d.ts.map +1 -0
- package/out/address/{point-schema.js → point/schema.js} +4 -20
- package/out/address/point/schema.js.map +1 -0
- package/out/address/point.d.ts +24 -21
- package/out/address/point.d.ts.map +1 -1
- package/out/address/point.js +60 -77
- package/out/address/point.js.map +1 -1
- package/out/{address/index.js → address.d.ts} +3 -3
- package/out/address.d.ts.map +1 -0
- package/out/{address/index.d.ts → address.js} +3 -3
- 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 +18 -76
- 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 +170 -0
- package/out/build/candidate.d.ts.map +1 -0
- package/out/{build-candidate.js → build/candidate.js} +33 -102
- package/out/build/candidate.js.map +1 -0
- 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 +44 -0
- package/out/build/slim.d.ts.map +1 -0
- package/out/{build-slim.js → build/slim.js} +33 -94
- package/out/build/slim.js.map +1 -0
- package/out/candidate/alias-bags.d.ts +13 -5
- package/out/candidate/alias-bags.d.ts.map +1 -1
- package/out/candidate/alias-bags.js +55 -12
- package/out/candidate/alias-bags.js.map +1 -1
- package/out/candidate/ancestors/schema.d.ts +166 -0
- package/out/candidate/ancestors/schema.d.ts.map +1 -0
- package/out/candidate/ancestors/schema.js +131 -0
- package/out/candidate/ancestors/schema.js.map +1 -0
- package/out/candidate/ancestors/sidecar.d.ts +27 -0
- package/out/candidate/ancestors/sidecar.d.ts.map +1 -0
- package/out/candidate/{ancestors-sidecar.js → ancestors/sidecar.js} +17 -24
- package/out/candidate/ancestors/sidecar.js.map +1 -0
- 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 +8 -16
- package/out/candidate/extract-fold.d.ts.map +1 -1
- package/out/candidate/extract-fold.js +6 -48
- package/out/candidate/extract-fold.js.map +1 -1
- package/out/candidate/fts.d.ts +19 -0
- package/out/candidate/fts.d.ts.map +1 -0
- package/out/candidate/fts.js +22 -0
- package/out/candidate/fts.js.map +1 -0
- package/out/candidate/importance.d.ts +127 -0
- package/out/candidate/importance.d.ts.map +1 -0
- package/out/candidate/importance.js +162 -0
- package/out/candidate/importance.js.map +1 -0
- package/out/candidate/lookup.d.ts +36 -0
- package/out/candidate/lookup.d.ts.map +1 -0
- package/out/candidate/lookup.js +456 -0
- package/out/candidate/lookup.js.map +1 -0
- package/out/candidate/name-roles.d.ts +10 -22
- 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 +34 -31
- 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 +120 -0
- package/out/candidate/schema.d.ts.map +1 -0
- package/out/{candidate-schema.js → candidate/schema.js} +11 -23
- package/out/candidate/schema.js.map +1 -0
- package/out/{candidate-scoring.d.ts → candidate/scoring.d.ts} +8 -9
- package/out/candidate/scoring.d.ts.map +1 -0
- package/out/candidate/scoring.js +157 -0
- package/out/candidate/scoring.js.map +1 -0
- 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 +26 -24
- 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 +61 -50
- package/out/coincident-roles.js.map +1 -1
- package/out/convention/schema.d.ts +12 -6
- 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 +33 -38
- package/out/coverage-manifest-schema.d.ts.map +1 -1
- package/out/coverage-manifest-schema.js +20 -29
- package/out/coverage-manifest-schema.js.map +1 -1
- package/out/currency-backfill.d.ts +13 -35
- package/out/currency-backfill.d.ts.map +1 -1
- package/out/currency-backfill.js +12 -39
- package/out/currency-backfill.js.map +1 -1
- package/out/env.d.ts +2 -2
- 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 +31 -20
- package/out/exact-match.js.map +1 -1
- package/out/extracts.d.ts +65 -54
- 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 +28 -20
- package/out/fst/autocomplete.d.ts.map +1 -1
- package/out/fst/autocomplete.js +38 -27
- package/out/fst/autocomplete.js.map +1 -1
- package/out/fst/builder.d.ts.map +1 -1
- package/out/fst/builder.js +30 -36
- package/out/fst/builder.js.map +1 -1
- package/out/fst/deserialize-web.d.ts +3 -3
- package/out/fst/deserialize-web.d.ts.map +1 -1
- package/out/fst/deserialize-web.js +8 -9
- 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 +11 -73
- package/out/fst/freshness.d.ts.map +1 -1
- package/out/fst/freshness.js +12 -76
- package/out/fst/freshness.js.map +1 -1
- package/out/fst/matcher.d.ts +1 -1
- 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 +30 -26
- package/out/fst/serialize.d.ts.map +1 -1
- package/out/fst/serialize.js +44 -58
- package/out/fst/serialize.js.map +1 -1
- package/out/fst/types.d.ts +71 -59
- 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} +1 -1
- package/out/fst.d.ts.map +1 -0
- package/out/{fst/index.d.ts → fst.js} +1 -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 -67
- package/out/geonames/aliases.d.ts.map +1 -1
- package/out/geonames/aliases.js +44 -121
- package/out/geonames/aliases.js.map +1 -1
- package/out/geonames/postal.d.ts +84 -50
- package/out/geonames/postal.d.ts.map +1 -1
- package/out/geonames/postal.js +115 -78
- 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 +20 -21
- package/out/index.d.ts.map +1 -1
- package/out/index.js +14 -15
- package/out/index.js.map +1 -1
- package/out/interpolation.d.ts +15 -52
- package/out/interpolation.d.ts.map +1 -1
- package/out/interpolation.js +25 -68
- package/out/interpolation.js.map +1 -1
- package/out/lookup.d.ts +24 -53
- package/out/lookup.d.ts.map +1 -1
- package/out/lookup.js +117 -223
- 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 +49 -141
- package/out/place-importance-schema.d.ts.map +1 -1
- package/out/place-importance-schema.js +25 -140
- package/out/place-importance-schema.js.map +1 -1
- package/out/poi/lookup.d.ts +38 -42
- package/out/poi/lookup.d.ts.map +1 -1
- package/out/poi/lookup.js +67 -90
- 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 → city/alias/lookup.d.ts} +22 -25
- package/out/postal/city/alias/lookup.d.ts.map +1 -0
- package/out/postal/city/alias/lookup.js +49 -0
- package/out/postal/city/alias/lookup.js.map +1 -0
- package/out/postal/{city-alias-schema.d.ts → city/alias/schema.d.ts} +15 -11
- package/out/postal/city/alias/schema.d.ts.map +1 -0
- package/out/postal/{city-alias-schema.js → city/alias/schema.js} +11 -9
- package/out/postal/city/alias/schema.js.map +1 -0
- package/out/postal/city/candidate-schema.d.ts +72 -0
- package/out/postal/city/candidate-schema.d.ts.map +1 -0
- package/out/postal/city/candidate-schema.js +60 -0
- package/out/postal/city/candidate-schema.js.map +1 -0
- package/out/postal.d.ts +9 -0
- package/out/postal.d.ts.map +1 -0
- package/out/postal.js +9 -0
- package/out/postal.js.map +1 -0
- package/out/postcode-point-lookup.d.ts +9 -19
- 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 +39 -93
- package/out/primary-preference.d.ts.map +1 -1
- package/out/primary-preference.js +13 -97
- package/out/primary-preference.js.map +1 -1
- package/out/proximity-rerank.d.ts +40 -29
- 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 +91 -62
- 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 +48 -29
- 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 +75 -50
- package/out/reverse.d.ts.map +1 -1
- package/out/reverse.js +61 -49
- package/out/reverse.js.map +1 -1
- package/out/schema.d.ts +74 -42
- 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 +25 -0
- package/out/sqlite/convention-source.d.ts.map +1 -0
- package/out/sqlite/convention-source.js +48 -0
- package/out/sqlite/convention-source.js.map +1 -0
- package/out/sqlite/utils.d.ts +45 -0
- package/out/sqlite/utils.d.ts.map +1 -0
- package/out/sqlite/utils.js +56 -0
- package/out/sqlite/utils.js.map +1 -0
- package/out/street/centroid/schema.d.ts +100 -0
- package/out/street/centroid/schema.d.ts.map +1 -0
- package/out/street/centroid/schema.js +66 -0
- package/out/street/centroid/schema.js.map +1 -0
- package/out/street/centroid.d.ts +9 -19
- package/out/street/centroid.d.ts.map +1 -1
- package/out/street/centroid.js +11 -22
- package/out/street/centroid.js.map +1 -1
- package/out/street/{morphology-fst-builder.d.ts → morphology/fst/builder.d.ts} +21 -11
- package/out/street/morphology/fst/builder.d.ts.map +1 -0
- package/out/street/{morphology-fst-builder.js → morphology/fst/builder.js} +40 -34
- package/out/street/morphology/fst/builder.js.map +1 -0
- package/out/street/morphology/fst/loader.d.ts +74 -0
- package/out/street/morphology/fst/loader.d.ts.map +1 -0
- package/out/street/{morphology-fst-loader.js → morphology/fst/loader.js} +21 -21
- package/out/street/morphology/fst/loader.js.map +1 -0
- 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 +32 -31
- 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 +74 -191
- 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.d.ts → street.d.ts} +4 -4
- package/out/street.d.ts.map +1 -0
- package/out/{street/index.js → street.js} +4 -4
- package/out/street.js.map +1 -0
- package/out/types.d.ts +69 -138
- 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 +20 -56
- 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 +132 -141
- package/out/weights-overlay-linker.d.ts.map +1 -1
- package/out/weights-overlay-linker.js +86 -154
- package/out/weights-overlay-linker.js.map +1 -1
- package/package.json +94 -599
- package/lib/address/point-schema.ts +0 -155
- package/lib/build-candidate.ts +0 -632
- package/lib/build-slim.ts +0 -408
- package/lib/candidate-ancestors-schema.ts +0 -195
- package/lib/candidate-fts.ts +0 -46
- package/lib/candidate-importance.ts +0 -224
- package/lib/candidate-lookup.ts +0 -977
- package/lib/candidate-schema.ts +0 -247
- package/lib/candidate-scoring.ts +0 -261
- package/lib/convention/index.ts +0 -154
- package/lib/fts/index.ts +0 -361
- package/lib/postal/city-alias-lookup.ts +0 -97
- package/lib/postal/index.ts +0 -9
- package/lib/sqlite-convention-source.ts +0 -65
- package/lib/sqlite-utils.ts +0 -82
- package/lib/street/centroid-schema.ts +0 -154
- package/out/address/index.d.ts.map +0 -1
- package/out/address/index.js.map +0 -1
- package/out/address/point-interpolation.d.ts +0 -49
- package/out/address/point-interpolation.d.ts.map +0 -1
- package/out/address/point-interpolation.js.map +0 -1
- package/out/address/point-schema.d.ts +0 -97
- package/out/address/point-schema.d.ts.map +0 -1
- package/out/address/point-schema.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/build-candidate.d.ts +0 -184
- package/out/build-candidate.d.ts.map +0 -1
- package/out/build-candidate.js.map +0 -1
- package/out/build-slim.d.ts +0 -80
- package/out/build-slim.d.ts.map +0 -1
- package/out/build-slim.js.map +0 -1
- package/out/candidate/ancestors-sidecar.d.ts +0 -32
- package/out/candidate/ancestors-sidecar.d.ts.map +0 -1
- package/out/candidate/ancestors-sidecar.js.map +0 -1
- package/out/candidate-ancestors-schema.d.ts +0 -150
- package/out/candidate-ancestors-schema.d.ts.map +0 -1
- package/out/candidate-ancestors-schema.js +0 -123
- package/out/candidate-ancestors-schema.js.map +0 -1
- package/out/candidate-fts.d.ts +0 -37
- package/out/candidate-fts.d.ts.map +0 -1
- package/out/candidate-fts.js +0 -40
- package/out/candidate-fts.js.map +0 -1
- package/out/candidate-importance.d.ts +0 -132
- package/out/candidate-importance.d.ts.map +0 -1
- package/out/candidate-importance.js +0 -169
- package/out/candidate-importance.js.map +0 -1
- package/out/candidate-lookup.d.ts +0 -76
- package/out/candidate-lookup.d.ts.map +0 -1
- package/out/candidate-lookup.js +0 -768
- package/out/candidate-lookup.js.map +0 -1
- package/out/candidate-schema.d.ts +0 -155
- package/out/candidate-schema.d.ts.map +0 -1
- package/out/candidate-schema.js.map +0 -1
- package/out/candidate-scoring.d.ts.map +0 -1
- package/out/candidate-scoring.js +0 -196
- package/out/candidate-scoring.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/city-alias-lookup.d.ts.map +0 -1
- package/out/postal/city-alias-lookup.js +0 -62
- package/out/postal/city-alias-lookup.js.map +0 -1
- package/out/postal/city-alias-schema.d.ts.map +0 -1
- package/out/postal/city-alias-schema.js.map +0 -1
- package/out/postal/city-candidate-schema.d.ts +0 -66
- package/out/postal/city-candidate-schema.d.ts.map +0 -1
- package/out/postal/city-candidate-schema.js +0 -56
- package/out/postal/city-candidate-schema.js.map +0 -1
- package/out/postal/index.d.ts +0 -9
- package/out/postal/index.d.ts.map +0 -1
- package/out/postal/index.js +0 -9
- package/out/postal/index.js.map +0 -1
- package/out/sqlite-convention-source.d.ts +0 -29
- package/out/sqlite-convention-source.d.ts.map +0 -1
- package/out/sqlite-convention-source.js +0 -57
- package/out/sqlite-convention-source.js.map +0 -1
- package/out/sqlite-utils.d.ts +0 -45
- package/out/sqlite-utils.d.ts.map +0 -1
- package/out/sqlite-utils.js +0 -57
- package/out/sqlite-utils.js.map +0 -1
- package/out/street/centroid-schema.d.ts +0 -105
- package/out/street/centroid-schema.d.ts.map +0 -1
- package/out/street/centroid-schema.js +0 -78
- package/out/street/centroid-schema.js.map +0 -1
- package/out/street/index.d.ts.map +0 -1
- package/out/street/index.js.map +0 -1
- package/out/street/morphology-fst-builder.d.ts.map +0 -1
- package/out/street/morphology-fst-builder.js.map +0 -1
- package/out/street/morphology-fst-loader.d.ts +0 -67
- package/out/street/morphology-fst-loader.d.ts.map +0 -1
- package/out/street/morphology-fst-loader.js.map +0 -1
- package/out/uprn/index.d.ts.map +0 -1
- package/out/uprn/index.js.map +0 -1
- /package/lib/{fst/index.ts → fst.ts} +0 -0
- /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/{uprn/index.ts → uprn.ts} +0 -0
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { expandPlacetypeFilter } from "@mailwoman/codex/placetype-map"
|
|
8
|
+
import { type Ancestor, type GazetteerArtifactCoverage, referentialFromPopulation } from "@mailwoman/core/resolver"
|
|
9
|
+
import { allRows } from "@mailwoman/core/utils"
|
|
10
|
+
import { jaroWinkler, levenshteinSimilarity } from "@mailwoman/match/comparators"
|
|
11
|
+
import { partitionByContainment } from "@mailwoman/resolver"
|
|
12
|
+
import { haversineKm } from "@mailwoman/spatial"
|
|
13
|
+
import type { DatabaseClient } from "@mailwoman/sqlite/client"
|
|
14
|
+
import { SQLiteLookup, type SQLiteLookupOptions } from "@mailwoman/sqlite/lookup"
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
CANDIDATE_ANCESTOR_TABLE,
|
|
18
|
+
CANDIDATE_INTERVAL_TABLE,
|
|
19
|
+
intervalContains,
|
|
20
|
+
type CandidateAncestorTable,
|
|
21
|
+
type IntervalLabel,
|
|
22
|
+
} from "#candidate/ancestors/schema"
|
|
23
|
+
import { CANDIDATE_FTS_TABLE } from "#candidate/fts"
|
|
24
|
+
import type { CandidateDatabase, CandidateTable, CountryCodeTable, PlacetypeCodeTable } from "#candidate/schema"
|
|
25
|
+
import { readGazetteerCoverageManifest } from "#coverage-manifest-schema"
|
|
26
|
+
import { POSTAL_CITY_CANDIDATE_TABLE, type PostalCityCandidateTable } from "#postal/city/candidate-schema"
|
|
27
|
+
import { rankByPrimaryPreference, type RankedRow, RERANK_FETCH } from "#primary-preference"
|
|
28
|
+
import { applyProximityRerank } from "#proximity-rerank"
|
|
29
|
+
import { REGION_CLASS_PLACETYPES, regionQualifierProbeKeys } from "#region-keys"
|
|
30
|
+
import { hasColumn, hasTable } from "#sqlite-utils"
|
|
31
|
+
import { type NameKey, normalizeLocalityForKey, stripLocalityQualifier } from "#street/normalize"
|
|
32
|
+
import type { FindPlaceQuery, PlaceCandidate, PlaceLookup, WOFPlacetype } from "#types"
|
|
33
|
+
|
|
34
|
+
export { rankByPrimaryPreference } from "#primary-preference"
|
|
35
|
+
export type { RankedRow } from "#primary-preference"
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Where a {@link WOFCandidateTableLookup} reads from
|
|
39
|
+
* (a read-only `candidate.db` or a caller-held connection) and how it ranks.
|
|
40
|
+
*/
|
|
41
|
+
export interface WOFCandidateTableLookupOpts extends SQLiteLookupOptions<CandidateDatabase> {
|
|
42
|
+
/**
|
|
43
|
+
* Exempt `name_role = 'variant'` aliases from the cross-country primary-preference penalty.
|
|
44
|
+
* No-ops without the role column and is off by default.
|
|
45
|
+
*/
|
|
46
|
+
variantAliasExemption?: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type CandidateRow = Pick<
|
|
50
|
+
CandidateTable,
|
|
51
|
+
| "spr_id"
|
|
52
|
+
| "name"
|
|
53
|
+
| "country_id"
|
|
54
|
+
| "placetype_id"
|
|
55
|
+
| "latitude"
|
|
56
|
+
| "longitude"
|
|
57
|
+
| "min_lat"
|
|
58
|
+
| "min_lon"
|
|
59
|
+
| "max_lat"
|
|
60
|
+
| "max_lon"
|
|
61
|
+
| "neg_rank"
|
|
62
|
+
| "is_primary"
|
|
63
|
+
| "population"
|
|
64
|
+
> &
|
|
65
|
+
// Optional because older artifacts may not include `importance`.
|
|
66
|
+
Partial<Pick<CandidateTable, "importance">>
|
|
67
|
+
|
|
68
|
+
const FUZZY_FETCH = 40
|
|
69
|
+
|
|
70
|
+
const WORD_FUZZY_MIN = 0.85
|
|
71
|
+
|
|
72
|
+
function wordFuzzySimilarity(a: string, b: string): number {
|
|
73
|
+
return Math.max(jaroWinkler(a, b), levenshteinSimilarity(a, b))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const POSTCODE_CONTAINMENT_THRESHOLD_KM = 25
|
|
77
|
+
|
|
78
|
+
function ftsTrigramQuery(s: string): string {
|
|
79
|
+
const grams = new Set<string>()
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i + 3 <= s.length; i++) {
|
|
82
|
+
const g = s.slice(i, i + 3)
|
|
83
|
+
|
|
84
|
+
if (/^[\p{L}\p{N} ]{3}$/u.test(g)) {
|
|
85
|
+
grams.add(g)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return [...grams].map((g) => `"${g}"`).join(" OR ")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Node {@link PlaceLookup} over `candidate.db`.
|
|
94
|
+
*/
|
|
95
|
+
export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> implements PlaceLookup {
|
|
96
|
+
readonly #countryToID = new Map<string, number>()
|
|
97
|
+
readonly #idToCountry = new Map<number, string>()
|
|
98
|
+
readonly #placetypeToID = new Map<string, number>()
|
|
99
|
+
readonly #idToPlacetype = new Map<number, string>()
|
|
100
|
+
|
|
101
|
+
readonly #postalCityProbe: ReturnType<DatabaseClient["prepare"]> | undefined
|
|
102
|
+
|
|
103
|
+
readonly #ftsProbe: ReturnType<DatabaseClient["prepare"]> | undefined
|
|
104
|
+
/**
|
|
105
|
+
* Prepared unfiltered `name_key` existence probe, so fuzzy fallback runs only for true name misses.
|
|
106
|
+
*/
|
|
107
|
+
readonly #nameKeyExistsProbe: ReturnType<DatabaseClient["prepare"]> | undefined
|
|
108
|
+
|
|
109
|
+
readonly artifactCoverage: GazetteerArtifactCoverage | undefined
|
|
110
|
+
|
|
111
|
+
readonly #importanceSelect: string
|
|
112
|
+
|
|
113
|
+
readonly #hasNameRole: boolean
|
|
114
|
+
readonly #variantAliasExemption: boolean
|
|
115
|
+
|
|
116
|
+
readonly #roleSelect: string
|
|
117
|
+
|
|
118
|
+
readonly #ancestorsProbe: ReturnType<DatabaseClient["prepare"]> | undefined
|
|
119
|
+
readonly #ancestorsCache = new Map<number, Ancestor[]>()
|
|
120
|
+
|
|
121
|
+
readonly #intervalProbe: ReturnType<DatabaseClient["prepare"]> | undefined
|
|
122
|
+
readonly #intervalCache = new Map<number, IntervalLabel | null>()
|
|
123
|
+
|
|
124
|
+
readonly #qualifierProbe: ReturnType<DatabaseClient["prepare"]> | undefined
|
|
125
|
+
/**
|
|
126
|
+
* Ancestor lineage accessor, nearest-first, present only when the artifact supports it.
|
|
127
|
+
*/
|
|
128
|
+
readonly ancestors: ((id: number | string) => Ancestor[]) | undefined
|
|
129
|
+
|
|
130
|
+
constructor(opts: WOFCandidateTableLookupOpts) {
|
|
131
|
+
super(opts)
|
|
132
|
+
|
|
133
|
+
for (const r of allRows<CountryCodeTable>(this.database.prepare("SELECT id, code FROM country_codes"))) {
|
|
134
|
+
const code = String(r.code).toUpperCase()
|
|
135
|
+
this.#countryToID.set(code, Number(r.id))
|
|
136
|
+
this.#idToCountry.set(Number(r.id), code)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const r of allRows<PlacetypeCodeTable>(this.database.prepare("SELECT id, placetype FROM placetype_codes"))) {
|
|
140
|
+
this.#placetypeToID.set(String(r.placetype), Number(r.id))
|
|
141
|
+
this.#idToPlacetype.set(Number(r.id), String(r.placetype))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (hasTable(this.database, POSTAL_CITY_CANDIDATE_TABLE)) {
|
|
145
|
+
this.#postalCityProbe = this.database.prepare(
|
|
146
|
+
`SELECT spr_id, name, latitude, longitude FROM ${POSTAL_CITY_CANDIDATE_TABLE} WHERE name_key = ? AND postcode = ? LIMIT 1`
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (hasTable(this.database, CANDIDATE_FTS_TABLE)) {
|
|
151
|
+
this.#ftsProbe = this.database.prepare(
|
|
152
|
+
`SELECT name_key FROM ${CANDIDATE_FTS_TABLE} WHERE ${CANDIDATE_FTS_TABLE} MATCH ? ORDER BY bm25(${CANDIDATE_FTS_TABLE}) LIMIT ?`
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
this.#nameKeyExistsProbe = this.database.prepare("SELECT 1 FROM candidate WHERE name_key = ? LIMIT 1")
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Optional columns are detected once here for hot-path reads.
|
|
159
|
+
this.#importanceSelect = hasColumn(this.database, "candidate", "importance") ? ", importance" : ""
|
|
160
|
+
this.#hasNameRole = hasColumn(this.database, "candidate", "name_role")
|
|
161
|
+
this.#variantAliasExemption = opts.variantAliasExemption === true
|
|
162
|
+
this.#roleSelect = this.#hasNameRole ? ", name_role" : ""
|
|
163
|
+
|
|
164
|
+
if (hasTable(this.database, CANDIDATE_ANCESTOR_TABLE)) {
|
|
165
|
+
this.#ancestorsProbe = this.database.prepare(
|
|
166
|
+
`SELECT parent_spr_id, parent_placetype_id, parent_name FROM ${CANDIDATE_ANCESTOR_TABLE}` +
|
|
167
|
+
" WHERE spr_id = ? ORDER BY depth ASC"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
this.ancestors = (id) => this.#ancestorLineage(id)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (this.#ancestorsProbe && hasTable(this.database, CANDIDATE_INTERVAL_TABLE)) {
|
|
174
|
+
this.#intervalProbe = this.database.prepare(`SELECT pre, post FROM ${CANDIDATE_INTERVAL_TABLE} WHERE spr_id = ?`)
|
|
175
|
+
|
|
176
|
+
const bandIDs = [...REGION_CLASS_PLACETYPES, "country"]
|
|
177
|
+
.map((placetype) => this.#placetypeToID.get(placetype))
|
|
178
|
+
.filter((id): id is number => id !== undefined)
|
|
179
|
+
|
|
180
|
+
if (bandIDs.length) {
|
|
181
|
+
this.#qualifierProbe = this.database.prepare(
|
|
182
|
+
`SELECT DISTINCT spr_id FROM candidate WHERE name_key = ? AND placetype_id IN (${bandIDs.join(",")}) LIMIT 8`
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.artifactCoverage = readGazetteerCoverageManifest(this.database)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#ancestorLineage(id: number | string): Ancestor[] {
|
|
191
|
+
const pid = typeof id === "number" ? id : Number(id)
|
|
192
|
+
|
|
193
|
+
if (!Number.isFinite(pid) || !this.#ancestorsProbe) return []
|
|
194
|
+
|
|
195
|
+
const cached = this.#ancestorsCache.get(pid)
|
|
196
|
+
|
|
197
|
+
if (cached) return cached
|
|
198
|
+
|
|
199
|
+
const rows = allRows<Pick<CandidateAncestorTable, "parent_spr_id" | "parent_placetype_id" | "parent_name">>(
|
|
200
|
+
this.#ancestorsProbe,
|
|
201
|
+
pid
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
const lineage: Ancestor[] = rows.map((r) => ({
|
|
205
|
+
id: Number(r.parent_spr_id),
|
|
206
|
+
placetype: this.#idToPlacetype.get(Number(r.parent_placetype_id)) ?? "",
|
|
207
|
+
name: String(r.parent_name ?? ""),
|
|
208
|
+
}))
|
|
209
|
+
|
|
210
|
+
this.#ancestorsCache.set(pid, lineage)
|
|
211
|
+
|
|
212
|
+
return lineage
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#intervalLabel(sprID: number): IntervalLabel | null {
|
|
216
|
+
if (!this.#intervalProbe) return null
|
|
217
|
+
|
|
218
|
+
const cached = this.#intervalCache.get(sprID)
|
|
219
|
+
|
|
220
|
+
if (cached !== undefined) return cached
|
|
221
|
+
|
|
222
|
+
const row = this.#intervalProbe.get(sprID) as { pre: number; post: number } | undefined
|
|
223
|
+
const label = row ? { pre: Number(row.pre), post: Number(row.post) } : null
|
|
224
|
+
|
|
225
|
+
this.#intervalCache.set(sprID, label)
|
|
226
|
+
|
|
227
|
+
return label
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#qualifierRegionIDs(qualifier: string, country: string | undefined): Set<number> {
|
|
231
|
+
const ids = new Set<number>()
|
|
232
|
+
|
|
233
|
+
if (!this.#qualifierProbe) return ids
|
|
234
|
+
|
|
235
|
+
for (const key of regionQualifierProbeKeys(qualifier, country)) {
|
|
236
|
+
if (!key) continue
|
|
237
|
+
|
|
238
|
+
for (const row of allRows<{ spr_id: number }>(this.#qualifierProbe, key)) {
|
|
239
|
+
ids.add(Number(row.spr_id))
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return ids
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Whether `sprID` is contained by any qualifier row, using interval labels first
|
|
248
|
+
* and the ancestor chain as fallback.
|
|
249
|
+
*/
|
|
250
|
+
#containedByQualifier(sprID: number, qualifierIDs: ReadonlySet<number>, qualifierLabels: IntervalLabel[]): boolean {
|
|
251
|
+
if (qualifierIDs.has(sprID)) return true
|
|
252
|
+
|
|
253
|
+
const label = this.#intervalLabel(sprID)
|
|
254
|
+
|
|
255
|
+
if (label && qualifierLabels.some((outer) => intervalContains(outer, label))) return true
|
|
256
|
+
|
|
257
|
+
return this.#ancestorLineage(sprID).some((ancestor) => qualifierIDs.has(Number(ancestor.id)))
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Apply admin-containment re-rank: stamp containment, inject contained misses,
|
|
262
|
+
* then partition contained-first.
|
|
263
|
+
*/
|
|
264
|
+
#applyAdminContainment(
|
|
265
|
+
rows: Array<RankedRow<CandidateRow>>,
|
|
266
|
+
qualifier: string,
|
|
267
|
+
country: string | undefined,
|
|
268
|
+
opts: {
|
|
269
|
+
nameKey: NameKey
|
|
270
|
+
strippedKey: NameKey
|
|
271
|
+
shapeFilters: string[]
|
|
272
|
+
shapeParams: Array<string | number>
|
|
273
|
+
limit: number
|
|
274
|
+
}
|
|
275
|
+
): Array<RankedRow<CandidateRow>> {
|
|
276
|
+
const qualifierIDs = this.#qualifierRegionIDs(qualifier, country)
|
|
277
|
+
|
|
278
|
+
if (!qualifierIDs.size) {
|
|
279
|
+
for (const row of rows) {
|
|
280
|
+
row.containedByQualifier = false
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return rows
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const qualifierLabels = [...qualifierIDs]
|
|
287
|
+
.map((id) => this.#intervalLabel(id))
|
|
288
|
+
.filter((label): label is IntervalLabel => label !== null)
|
|
289
|
+
|
|
290
|
+
const contained = (sprID: number): boolean => this.#containedByQualifier(sprID, qualifierIDs, qualifierLabels)
|
|
291
|
+
|
|
292
|
+
for (const row of rows) {
|
|
293
|
+
row.containedByQualifier = contained(Number(row.spr_id))
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const present = new Set(rows.map((row) => Number(row.spr_id)))
|
|
297
|
+
const injected: Array<RankedRow<CandidateRow>> = []
|
|
298
|
+
|
|
299
|
+
const injectSQL = (primaryOnly: boolean): string =>
|
|
300
|
+
"SELECT spr_id, name, country_id, placetype_id, latitude, longitude, min_lat, min_lon, max_lat, max_lon, neg_rank, is_primary, population" +
|
|
301
|
+
`${this.#importanceSelect} FROM candidate WHERE ${["name_key = ?", ...opts.shapeFilters, ...(primaryOnly ? ["is_primary = 1"] : [])].join(" AND ")} ` +
|
|
302
|
+
"ORDER BY neg_rank ASC LIMIT ?"
|
|
303
|
+
|
|
304
|
+
const injectFrom = (key: string, primaryOnly: boolean): void => {
|
|
305
|
+
const fetched = allRows<CandidateRow>(
|
|
306
|
+
this.database.prepare(injectSQL(primaryOnly)),
|
|
307
|
+
key,
|
|
308
|
+
...opts.shapeParams,
|
|
309
|
+
RERANK_FETCH
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
for (const row of fetched) {
|
|
313
|
+
const sprID = Number(row.spr_id)
|
|
314
|
+
|
|
315
|
+
if (present.has(sprID) || !contained(sprID)) continue
|
|
316
|
+
present.add(sprID)
|
|
317
|
+
|
|
318
|
+
injected.push({ ...row, effectiveNegRank: row.neg_rank, demoted: false, containedByQualifier: true })
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
injectFrom(opts.nameKey, false)
|
|
323
|
+
|
|
324
|
+
// Also inject contained neighborhood-band namesakes (primary-keyed only).
|
|
325
|
+
const bandIDs = ["neighbourhood", "macrohood", "microhood"]
|
|
326
|
+
.map((placetype) => this.#placetypeToID.get(placetype))
|
|
327
|
+
.filter((id): id is number => id !== undefined)
|
|
328
|
+
|
|
329
|
+
if (bandIDs.length) {
|
|
330
|
+
const bandSQL =
|
|
331
|
+
"SELECT spr_id, name, country_id, placetype_id, latitude, longitude, min_lat, min_lon, max_lat, max_lon, neg_rank, is_primary, population" +
|
|
332
|
+
`${this.#importanceSelect} FROM candidate WHERE name_key = ? AND placetype_id IN (${bandIDs.map(() => "?").join(",")}) AND is_primary = 1 ` +
|
|
333
|
+
"ORDER BY neg_rank ASC LIMIT ?"
|
|
334
|
+
|
|
335
|
+
const fetched = allRows<CandidateRow>(this.database.prepare(bandSQL), opts.nameKey, ...bandIDs, RERANK_FETCH)
|
|
336
|
+
|
|
337
|
+
for (const row of fetched) {
|
|
338
|
+
const sprID = Number(row.spr_id)
|
|
339
|
+
|
|
340
|
+
if (present.has(sprID) || !contained(sprID)) continue
|
|
341
|
+
present.add(sprID)
|
|
342
|
+
|
|
343
|
+
injected.push({ ...row, effectiveNegRank: row.neg_rank, demoted: false, containedByQualifier: true })
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Try stripped-key injection only when exact-key containment found no row.
|
|
348
|
+
if (
|
|
349
|
+
!injected.length &&
|
|
350
|
+
!rows.some((row) => row.containedByQualifier) &&
|
|
351
|
+
opts.strippedKey &&
|
|
352
|
+
opts.strippedKey !== opts.nameKey
|
|
353
|
+
) {
|
|
354
|
+
injectFrom(opts.strippedKey, true)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (!injected.length && !rows.some((row) => row.containedByQualifier)) return rows
|
|
358
|
+
|
|
359
|
+
return partitionByContainment(
|
|
360
|
+
[...rows, ...injected],
|
|
361
|
+
(row) => row.containedByQualifier === true,
|
|
362
|
+
(row) => !row.demoted && !row.fuzzy
|
|
363
|
+
).slice(0, opts.limit)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
#wantsLocality(placetype: FindPlaceQuery["placetype"]): boolean {
|
|
367
|
+
if (!placetype) return true
|
|
368
|
+
const want = Array.isArray(placetype) ? placetype : [placetype]
|
|
369
|
+
|
|
370
|
+
return expandPlacetypeFilter(want as readonly string[]).includes("locality")
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
#postcodeAnchor(postcode: string, country?: string): { lat: number; lon: number } | null {
|
|
374
|
+
const placetypeID = this.#placetypeToID.get("postalcode")
|
|
375
|
+
|
|
376
|
+
if (placetypeID === undefined) return null
|
|
377
|
+
|
|
378
|
+
const conds = ["name_key = ?", "placetype_id = ?"]
|
|
379
|
+
const params: Array<string | number> = [postcode.replaceAll(/\s+/g, ""), placetypeID]
|
|
380
|
+
|
|
381
|
+
if (country) {
|
|
382
|
+
const countryID = this.#countryToID.get(country.toUpperCase())
|
|
383
|
+
|
|
384
|
+
if (countryID === undefined) return null // Unknown country in this artifact.
|
|
385
|
+
conds.push("country_id = ?")
|
|
386
|
+
params.push(countryID)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const row = this.database
|
|
390
|
+
.prepare(`SELECT latitude, longitude FROM candidate WHERE ${conds.join(" AND ")} ORDER BY neg_rank ASC LIMIT 1`)
|
|
391
|
+
.get(...params) as { latitude: number; longitude: number } | undefined
|
|
392
|
+
|
|
393
|
+
if (!row || (Number(row.latitude) === 0 && Number(row.longitude) === 0)) return null
|
|
394
|
+
|
|
395
|
+
return { lat: Number(row.latitude), lon: Number(row.longitude) }
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async findPlace(query: FindPlaceQuery): Promise<PlaceCandidate[]> {
|
|
399
|
+
let text = (query.text ?? "").trim()
|
|
400
|
+
|
|
401
|
+
if (!text) return []
|
|
402
|
+
|
|
403
|
+
// Postcode queries use whitespace-stripped keying.
|
|
404
|
+
const wantsPostcode = [query.placetype].flat().includes("postalcode")
|
|
405
|
+
|
|
406
|
+
if (wantsPostcode) {
|
|
407
|
+
text = text.replaceAll(/\s+/g, "")
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const nameKey = normalizeLocalityForKey(text)
|
|
411
|
+
|
|
412
|
+
if (!nameKey) return []
|
|
413
|
+
|
|
414
|
+
if (query.postcode && this.#postalCityProbe && this.#wantsLocality(query.placetype)) {
|
|
415
|
+
const hit = this.#postalCityProbe.get(nameKey, query.postcode.trim()) as
|
|
416
|
+
| Pick<PostalCityCandidateTable, "spr_id" | "name" | "latitude" | "longitude">
|
|
417
|
+
| undefined
|
|
418
|
+
|
|
419
|
+
if (hit) {
|
|
420
|
+
return [
|
|
421
|
+
{
|
|
422
|
+
id: Number(hit.spr_id),
|
|
423
|
+
name: String(hit.name ?? ""),
|
|
424
|
+
placetype: "locality" as WOFPlacetype,
|
|
425
|
+
country: query.country?.toUpperCase() ?? "",
|
|
426
|
+
lat: Number(hit.latitude),
|
|
427
|
+
lon: Number(hit.longitude),
|
|
428
|
+
score: 1,
|
|
429
|
+
exactMatch: true,
|
|
430
|
+
},
|
|
431
|
+
]
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const limit = Math.max(1, query.limit ?? 10)
|
|
436
|
+
|
|
437
|
+
// Shape filters are tracked separately from the shared country scope for exact and stripped probes.
|
|
438
|
+
const filters: string[] = []
|
|
439
|
+
const filterParams: Array<string | number> = []
|
|
440
|
+
const shapeFilters: string[] = []
|
|
441
|
+
const shapeParams: Array<string | number> = []
|
|
442
|
+
|
|
443
|
+
if (query.country) {
|
|
444
|
+
const cid = this.#countryToID.get(query.country.toUpperCase())
|
|
445
|
+
|
|
446
|
+
if (cid === undefined) return [] // Unknown country in this artifact.
|
|
447
|
+
filters.push("country_id = ?")
|
|
448
|
+
filterParams.push(cid)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (query.placetype) {
|
|
452
|
+
// Expand placetype equivalents (e.g. locality includes borough/localadmin).
|
|
453
|
+
const want = Array.isArray(query.placetype) ? query.placetype : [query.placetype]
|
|
454
|
+
|
|
455
|
+
const ids = expandPlacetypeFilter(want as readonly string[])
|
|
456
|
+
.map((t) => this.#placetypeToID.get(t))
|
|
457
|
+
.filter((v): v is number => v !== undefined)
|
|
458
|
+
|
|
459
|
+
if (!ids.length) return []
|
|
460
|
+
shapeFilters.push(`placetype_id IN (${ids.map(() => "?").join(",")})`)
|
|
461
|
+
shapeParams.push(...ids)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (query.bbox) {
|
|
465
|
+
const b = query.bbox
|
|
466
|
+
shapeFilters.push("latitude BETWEEN ? AND ? AND longitude BETWEEN ? AND ?")
|
|
467
|
+
shapeParams.push(b.minLat, b.maxLat, b.minLon, b.maxLon)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Optional guard: restrict to primary rows for token-extracted probes.
|
|
471
|
+
if (query.primaryOnly) {
|
|
472
|
+
shapeFilters.push("is_primary = 1")
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// The role filter is skipped on an artifact lacking `name_role`.
|
|
476
|
+
if (query.excludeNameRoles?.length && this.#hasNameRole) {
|
|
477
|
+
shapeFilters.push(`(name_role IS NULL OR name_role NOT IN (${query.excludeNameRoles.map(() => "?").join(",")}))`)
|
|
478
|
+
shapeParams.push(...query.excludeNameRoles)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
filters.push(...shapeFilters)
|
|
482
|
+
filterParams.push(...shapeParams)
|
|
483
|
+
|
|
484
|
+
// Region scope is optional (`region_id = parentID`); the fallback stays unscoped.
|
|
485
|
+
const regionParentID = query.parentID || undefined
|
|
486
|
+
|
|
487
|
+
const probe = (nk: string, regionID: number | undefined, countryID?: number): Array<RankedRow<CandidateRow>> => {
|
|
488
|
+
const conds = ["name_key = ?", ...filters]
|
|
489
|
+
const params: Array<string | number> = [nk, ...filterParams]
|
|
490
|
+
|
|
491
|
+
if (regionID !== undefined) {
|
|
492
|
+
conds.push("region_id = ?")
|
|
493
|
+
params.push(regionID)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (typeof countryID === "number") {
|
|
497
|
+
conds.push("country_id = ?")
|
|
498
|
+
params.push(countryID)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Population-ordered fetch over-fetches for the bounded primary-preference rerank.
|
|
502
|
+
const sql =
|
|
503
|
+
"SELECT spr_id, name, country_id, placetype_id, latitude, longitude, min_lat, min_lon, max_lat, max_lon, neg_rank, is_primary, population" +
|
|
504
|
+
`${this.#importanceSelect}${this.#roleSelect} FROM candidate WHERE ${conds.join(" AND ")} ORDER BY neg_rank ASC LIMIT ?`
|
|
505
|
+
|
|
506
|
+
const fetched = allRows<CandidateRow>(this.database.prepare(sql), ...params, Math.max(limit, RERANK_FETCH))
|
|
507
|
+
|
|
508
|
+
return rankByPrimaryPreference(fetched, limit, undefined, this.#idToPlacetype, this.#variantAliasExemption)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const cascade = (regionID: number | undefined): Array<RankedRow<CandidateRow>> => {
|
|
512
|
+
let rows = probe(nameKey, regionID)
|
|
513
|
+
|
|
514
|
+
if (!rows.length) {
|
|
515
|
+
const strippedKey = normalizeLocalityForKey(stripLocalityQualifier(text))
|
|
516
|
+
|
|
517
|
+
if (strippedKey && strippedKey !== nameKey) {
|
|
518
|
+
// Stripped probes only accept primary rows to avoid alias-scrape matches.
|
|
519
|
+
rows = probe(strippedKey, regionID).filter((r) => r.is_primary === 1)
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Typo-tolerant fallback for true name misses only, skipping postcodes and respecting the fuzzy-country scope.
|
|
524
|
+
const fuzzyCountryID =
|
|
525
|
+
!query.country && query.fuzzyCountry ? this.#countryToID.get(query.fuzzyCountry.toUpperCase()) : undefined
|
|
526
|
+
|
|
527
|
+
const fuzzyScopedOut = !query.country && !!query.fuzzyCountry && typeof fuzzyCountryID !== "number"
|
|
528
|
+
|
|
529
|
+
if (
|
|
530
|
+
!rows.length &&
|
|
531
|
+
!wantsPostcode &&
|
|
532
|
+
!fuzzyScopedOut &&
|
|
533
|
+
this.#ftsProbe &&
|
|
534
|
+
this.#nameKeyExistsProbe &&
|
|
535
|
+
!this.#nameKeyExistsProbe.get(nameKey)
|
|
536
|
+
) {
|
|
537
|
+
const match = ftsTrigramQuery(nameKey)
|
|
538
|
+
|
|
539
|
+
if (match) {
|
|
540
|
+
const hits = allRows<{ name_key: string }>(this.#ftsProbe, match, FUZZY_FETCH)
|
|
541
|
+
|
|
542
|
+
const ranked = hits
|
|
543
|
+
.map((h) => ({ nk: String(h.name_key), s: wordFuzzySimilarity(nameKey, String(h.name_key)) }))
|
|
544
|
+
.filter((h) => h.s >= WORD_FUZZY_MIN)
|
|
545
|
+
// oxlint-disable-next-line unicorn/no-array-sort -- sorts a freshly-built array. toSorted would double-allocate on a hot path
|
|
546
|
+
.sort((a, b) => b.s - a.s)
|
|
547
|
+
|
|
548
|
+
const seen = new Set<string>()
|
|
549
|
+
|
|
550
|
+
for (const h of ranked) {
|
|
551
|
+
if (seen.has(h.nk)) continue
|
|
552
|
+
seen.add(h.nk)
|
|
553
|
+
|
|
554
|
+
rows.push(...probe(h.nk, regionID, fuzzyCountryID).map((r) => ({ ...r, fuzzy: true })))
|
|
555
|
+
|
|
556
|
+
if (rows.length >= limit) break
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
rows = rows.slice(0, limit)
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return rows
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
let rows = cascade(regionParentID)
|
|
567
|
+
|
|
568
|
+
let regionScopeMiss = false
|
|
569
|
+
|
|
570
|
+
if (!rows.length && regionParentID !== undefined) {
|
|
571
|
+
rows = cascade(undefined)
|
|
572
|
+
regionScopeMiss = rows.length > 0
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (
|
|
576
|
+
query.postcode &&
|
|
577
|
+
query.postcodeContainmentCoherence === true &&
|
|
578
|
+
this.#wantsLocality(query.placetype) &&
|
|
579
|
+
rows.length > 1
|
|
580
|
+
) {
|
|
581
|
+
const anchor = this.#postcodeAnchor(query.postcode, query.country)
|
|
582
|
+
|
|
583
|
+
if (anchor) {
|
|
584
|
+
const insideThreshold: Array<{ row: RankedRow<CandidateRow>; distanceKm: number }> = []
|
|
585
|
+
const outsideThreshold: RankedRow<CandidateRow>[] = []
|
|
586
|
+
|
|
587
|
+
for (const row of rows) {
|
|
588
|
+
const distanceKm = haversineKm(anchor.lat, anchor.lon, Number(row.latitude), Number(row.longitude))
|
|
589
|
+
|
|
590
|
+
if (distanceKm <= POSTCODE_CONTAINMENT_THRESHOLD_KM) {
|
|
591
|
+
insideThreshold.push({ row, distanceKm })
|
|
592
|
+
} else {
|
|
593
|
+
outsideThreshold.push(row)
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (insideThreshold.length) {
|
|
598
|
+
// oxlint-disable-next-line unicorn/no-array-sort -- hot-path in-place sort
|
|
599
|
+
insideThreshold.sort((a, b) => a.distanceKm - b.distanceKm)
|
|
600
|
+
rows = [...insideThreshold.map(({ row }) => row), ...outsideThreshold]
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Admin-containment re-rank runs last.
|
|
606
|
+
if (query.regionQualifier?.trim() && this.#qualifierProbe && this.#wantsLocality(query.placetype)) {
|
|
607
|
+
rows = this.#applyAdminContainment(rows, query.regionQualifier.trim(), query.country, {
|
|
608
|
+
nameKey,
|
|
609
|
+
strippedKey: normalizeLocalityForKey(stripLocalityQualifier(text)),
|
|
610
|
+
shapeFilters,
|
|
611
|
+
shapeParams,
|
|
612
|
+
limit,
|
|
613
|
+
})
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const candidates = rows.map((row): PlaceCandidate => {
|
|
617
|
+
const hasBbox = row.min_lat != null && row.max_lat != null && row.min_lon != null && row.max_lon != null
|
|
618
|
+
const parent = this.#ancestorLineage(Number(row.spr_id))[0]
|
|
619
|
+
|
|
620
|
+
return {
|
|
621
|
+
id: Number(row.spr_id),
|
|
622
|
+
name: String(row.name ?? ""),
|
|
623
|
+
placetype: (this.#idToPlacetype.get(Number(row.placetype_id)) ?? "") as WOFPlacetype,
|
|
624
|
+
|
|
625
|
+
country: this.#idToCountry.get(Number(row.country_id)) ?? "",
|
|
626
|
+
lat: Number(row.latitude),
|
|
627
|
+
lon: Number(row.longitude),
|
|
628
|
+
|
|
629
|
+
...(parent ? { parent_id: Number(parent.id) } : {}),
|
|
630
|
+
|
|
631
|
+
score: -Number(row.neg_rank),
|
|
632
|
+
|
|
633
|
+
prominence: -Number(row.effectiveNegRank),
|
|
634
|
+
|
|
635
|
+
exactMatch: !row.demoted && !row.fuzzy,
|
|
636
|
+
|
|
637
|
+
...(regionScopeMiss ? { regionScopeMiss: true } : {}),
|
|
638
|
+
|
|
639
|
+
...(row.containedByQualifier === undefined ? {} : { containedByQualifier: row.containedByQualifier }),
|
|
640
|
+
|
|
641
|
+
...(row.variantExempted ? { variantAliasExempted: true as const } : {}),
|
|
642
|
+
|
|
643
|
+
...(row.population === null || row.population <= 0
|
|
644
|
+
? {}
|
|
645
|
+
: { population: row.population, referential: referentialFromPopulation(row.population) }),
|
|
646
|
+
|
|
647
|
+
...(typeof row.importance === "number" && Number.isFinite(row.importance)
|
|
648
|
+
? { importance: row.importance }
|
|
649
|
+
: {}),
|
|
650
|
+
...(hasBbox
|
|
651
|
+
? {
|
|
652
|
+
bbox: {
|
|
653
|
+
minLat: Number(row.min_lat),
|
|
654
|
+
maxLat: Number(row.max_lat),
|
|
655
|
+
minLon: Number(row.min_lon),
|
|
656
|
+
maxLon: Number(row.max_lon),
|
|
657
|
+
},
|
|
658
|
+
}
|
|
659
|
+
: {}),
|
|
660
|
+
}
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
if (query.bias && query.bias.length) {
|
|
664
|
+
applyProximityRerank(candidates, query.bias)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return candidates
|
|
668
|
+
}
|
|
669
|
+
}
|