@mailwoman/corpus 8.6.0 → 9.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 +2 -2
- package/data/PROVENANCE.md +214 -0
- package/data/sub-venue-lexicon.json +18269 -0
- package/out/src/adapters/ban/adapter.d.ts +5 -4
- package/out/src/adapters/ban/adapter.d.ts.map +1 -1
- package/out/src/adapters/ban/adapter.js +62 -69
- package/out/src/adapters/ban/adapter.js.map +1 -1
- package/out/src/adapters/ban/street-decompose.d.ts.map +1 -1
- package/out/src/adapters/ban/street-decompose.js +18 -25
- package/out/src/adapters/ban/street-decompose.js.map +1 -1
- package/out/src/adapters/fcc-bdc/adapter.d.ts +1 -7
- package/out/src/adapters/fcc-bdc/adapter.d.ts.map +1 -1
- package/out/src/adapters/fcc-bdc/adapter.js +4 -26
- package/out/src/adapters/fcc-bdc/adapter.js.map +1 -1
- package/out/src/adapters/geonames/adapter.d.ts +1 -1
- package/out/src/adapters/geonames/adapter.d.ts.map +1 -1
- package/out/src/adapters/geonames/adapter.js +72 -78
- package/out/src/adapters/geonames/adapter.js.map +1 -1
- package/out/src/adapters/geonames-postal/adapter.d.ts +1 -1
- package/out/src/adapters/geonames-postal/adapter.d.ts.map +1 -1
- package/out/src/adapters/geonames-postal/adapter.js +46 -51
- package/out/src/adapters/geonames-postal/adapter.js.map +1 -1
- package/out/src/adapters/gnaf/adapter.d.ts +1 -1
- package/out/src/adapters/gnaf/adapter.d.ts.map +1 -1
- package/out/src/adapters/gnaf/adapter.js +6 -9
- package/out/src/adapters/gnaf/adapter.js.map +1 -1
- package/out/src/adapters/gnaf/assemble.d.ts.map +1 -1
- package/out/src/adapters/gnaf/assemble.js +7 -12
- package/out/src/adapters/gnaf/assemble.js.map +1 -1
- package/out/src/adapters/index.d.ts +1 -1
- package/out/src/adapters/index.d.ts.map +1 -1
- package/out/src/adapters/index.js +1 -1
- package/out/src/adapters/index.js.map +1 -1
- package/out/src/adapters/openaddresses/adapter.d.ts +1 -1
- package/out/src/adapters/openaddresses/adapter.d.ts.map +1 -1
- package/out/src/adapters/openaddresses/adapter.js +5 -10
- package/out/src/adapters/openaddresses/adapter.js.map +1 -1
- package/out/src/adapters/overture/adapter.d.ts +1 -1
- package/out/src/adapters/overture/adapter.d.ts.map +1 -1
- package/out/src/adapters/overture/adapter.js +5 -9
- package/out/src/adapters/overture/adapter.js.map +1 -1
- package/out/src/adapters/state-hi-schools/adapter.d.ts +1 -1
- package/out/src/adapters/state-hi-schools/adapter.d.ts.map +1 -1
- package/out/src/adapters/state-hi-schools/adapter.js +57 -75
- package/out/src/adapters/state-hi-schools/adapter.js.map +1 -1
- package/out/src/adapters/state-ia-contractors/adapter.d.ts +1 -1
- package/out/src/adapters/state-ia-contractors/adapter.d.ts.map +1 -1
- package/out/src/adapters/state-ia-contractors/adapter.js +60 -78
- package/out/src/adapters/state-ia-contractors/adapter.js.map +1 -1
- package/out/src/adapters/state-ny-notaries/adapter.d.ts +1 -1
- package/out/src/adapters/state-ny-notaries/adapter.d.ts.map +1 -1
- package/out/src/adapters/state-ny-notaries/adapter.js +69 -87
- package/out/src/adapters/state-ny-notaries/adapter.js.map +1 -1
- package/out/src/adapters/state-tx-notaries/adapter.d.ts +1 -1
- package/out/src/adapters/state-tx-notaries/adapter.d.ts.map +1 -1
- package/out/src/adapters/state-tx-notaries/adapter.js +71 -89
- package/out/src/adapters/state-tx-notaries/adapter.js.map +1 -1
- package/out/src/adapters/synth-po-box/adapter.d.ts +3 -3
- package/out/src/adapters/synth-po-box/adapter.d.ts.map +1 -1
- package/out/src/adapters/synth-po-box/adapter.js +10 -18
- package/out/src/adapters/synth-po-box/adapter.js.map +1 -1
- package/out/src/adapters/tiger/adapter.d.ts +1 -1
- package/out/src/adapters/tiger/adapter.d.ts.map +1 -1
- package/out/src/adapters/tiger/adapter.js +2 -2
- package/out/src/adapters/tiger/adapter.js.map +1 -1
- package/out/src/adapters/tiger/street-decompose.d.ts.map +1 -1
- package/out/src/adapters/tiger/street-decompose.js +18 -27
- package/out/src/adapters/tiger/street-decompose.js.map +1 -1
- package/out/src/adapters/usgov-hrsa-fqhc/adapter.d.ts +4 -4
- package/out/src/adapters/usgov-hrsa-fqhc/adapter.d.ts.map +1 -1
- package/out/src/adapters/usgov-hrsa-fqhc/adapter.js +64 -82
- package/out/src/adapters/usgov-hrsa-fqhc/adapter.js.map +1 -1
- package/out/src/adapters/usgov-imls-pls/adapter.d.ts +1 -1
- package/out/src/adapters/usgov-imls-pls/adapter.d.ts.map +1 -1
- package/out/src/adapters/usgov-imls-pls/adapter.js +62 -84
- package/out/src/adapters/usgov-imls-pls/adapter.js.map +1 -1
- package/out/src/adapters/usgov-irs-bmf/adapter.d.ts +1 -1
- package/out/src/adapters/usgov-irs-bmf/adapter.d.ts.map +1 -1
- package/out/src/adapters/usgov-irs-bmf/adapter.js +59 -66
- package/out/src/adapters/usgov-irs-bmf/adapter.js.map +1 -1
- package/out/src/adapters/usgov-nad/adapter.d.ts +1 -1
- package/out/src/adapters/usgov-nad/adapter.d.ts.map +1 -1
- package/out/src/adapters/usgov-nad/adapter.js +6 -9
- package/out/src/adapters/usgov-nad/adapter.js.map +1 -1
- package/out/src/adapters/usgov-nppes/adapter.d.ts +1 -1
- package/out/src/adapters/usgov-nppes/adapter.d.ts.map +1 -1
- package/out/src/adapters/usgov-nppes/adapter.js +60 -78
- package/out/src/adapters/usgov-nppes/adapter.js.map +1 -1
- package/out/src/adapters/usgov-samhsa-treatment-locator/adapter.d.ts +1 -1
- package/out/src/adapters/usgov-samhsa-treatment-locator/adapter.d.ts.map +1 -1
- package/out/src/adapters/usgov-samhsa-treatment-locator/adapter.js +53 -73
- package/out/src/adapters/usgov-samhsa-treatment-locator/adapter.js.map +1 -1
- package/out/src/{adapter.d.ts → adapters/utils/index.d.ts} +43 -2
- package/out/src/adapters/utils/index.d.ts.map +1 -0
- package/out/src/{adapter.js → adapters/utils/index.js} +47 -2
- package/out/src/adapters/utils/index.js.map +1 -0
- package/out/src/adapters/wof-admin-jp/adapter.d.ts +1 -1
- package/out/src/adapters/wof-admin-jp/adapter.d.ts.map +1 -1
- package/out/src/adapters/wof-admin-jp/adapter.js +24 -6
- package/out/src/adapters/wof-admin-jp/adapter.js.map +1 -1
- package/out/src/adapters/wof-admin-json/adapter.d.ts +2 -2
- package/out/src/adapters/wof-admin-json/adapter.d.ts.map +1 -1
- package/out/src/adapters/wof-admin-json/adapter.js +2 -2
- package/out/src/adapters/wof-admin-json/adapter.js.map +1 -1
- package/out/src/adapters/wof-postalcode-json/adapter.d.ts +2 -2
- package/out/src/adapters/wof-postalcode-json/adapter.d.ts.map +1 -1
- package/out/src/adapters/wof-postalcode-json/adapter.js +2 -2
- package/out/src/adapters/wof-postalcode-json/adapter.js.map +1 -1
- package/out/src/build.d.ts +3 -4
- package/out/src/build.d.ts.map +1 -1
- package/out/src/build.js +6 -8
- package/out/src/build.js.map +1 -1
- package/out/src/index.d.ts +3 -17
- package/out/src/index.d.ts.map +1 -1
- package/out/src/index.js +3 -17
- package/out/src/index.js.map +1 -1
- package/out/src/name-prone-us-suffixes.d.ts +12 -0
- package/out/src/name-prone-us-suffixes.d.ts.map +1 -0
- package/out/src/name-prone-us-suffixes.js +12 -0
- package/out/src/name-prone-us-suffixes.js.map +1 -0
- package/out/src/parquet-wrapper/reader.d.ts +8 -0
- package/out/src/parquet-wrapper/reader.d.ts.map +1 -1
- package/out/src/parquet-wrapper/reader.js +16 -0
- package/out/src/parquet-wrapper/reader.js.map +1 -1
- package/out/src/parquet-wrapper/schema.d.ts +4 -0
- package/out/src/parquet-wrapper/schema.d.ts.map +1 -1
- package/out/src/parquet-wrapper/schema.js.map +1 -1
- package/out/src/runner.d.ts +2 -2
- package/out/src/runner.d.ts.map +1 -1
- package/out/src/runner.js +1 -1
- package/out/src/runner.js.map +1 -1
- package/out/src/shard-recipes/anchor-absorption.d.ts.map +1 -1
- package/out/src/shard-recipes/anchor-absorption.js +5 -4
- package/out/src/shard-recipes/anchor-absorption.js.map +1 -1
- package/out/src/shard-recipes/boundary-stress.d.ts.map +1 -1
- package/out/src/shard-recipes/boundary-stress.js +2 -2
- package/out/src/shard-recipes/boundary-stress.js.map +1 -1
- package/out/src/shard-recipes/country-balanced.d.ts.map +1 -1
- package/out/src/shard-recipes/country-balanced.js +75 -75
- package/out/src/shard-recipes/country-balanced.js.map +1 -1
- package/out/src/shard-recipes/fr-admin-split.js +2 -2
- package/out/src/shard-recipes/fr-admin-split.js.map +1 -1
- package/out/src/shard-recipes/fr-fragment.d.ts.map +1 -1
- package/out/src/shard-recipes/fr-fragment.js +8 -5
- package/out/src/shard-recipes/fr-fragment.js.map +1 -1
- package/out/src/shard-recipes/fr-lieudit.js +2 -2
- package/out/src/shard-recipes/fr-lieudit.js.map +1 -1
- package/out/src/shard-recipes/fr-order.d.ts.map +1 -1
- package/out/src/shard-recipes/fr-order.js +18 -72
- package/out/src/shard-recipes/fr-order.js.map +1 -1
- package/out/src/shard-recipes/german.d.ts.map +1 -1
- package/out/src/shard-recipes/german.js +17 -70
- package/out/src/shard-recipes/german.js.map +1 -1
- package/out/src/shard-recipes/house-venue.d.ts.map +1 -1
- package/out/src/shard-recipes/house-venue.js +1 -1
- package/out/src/shard-recipes/house-venue.js.map +1 -1
- package/out/src/shard-recipes/index.d.ts.map +1 -1
- package/out/src/shard-recipes/index.js +4 -1
- package/out/src/shard-recipes/index.js.map +1 -1
- package/out/src/shard-recipes/intersection.d.ts +2 -2
- package/out/src/shard-recipes/intersection.d.ts.map +1 -1
- package/out/src/shard-recipes/intersection.js +28 -72
- package/out/src/shard-recipes/intersection.js.map +1 -1
- package/out/src/shard-recipes/locale.d.ts +5 -5
- package/out/src/shard-recipes/locale.d.ts.map +1 -1
- package/out/src/shard-recipes/locale.js +25 -35
- package/out/src/shard-recipes/locale.js.map +1 -1
- package/out/src/shard-recipes/no-fragment.d.ts.map +1 -1
- package/out/src/shard-recipes/no-fragment.js +8 -5
- package/out/src/shard-recipes/no-fragment.js.map +1 -1
- package/out/src/shard-recipes/no-street-led.d.ts.map +1 -1
- package/out/src/shard-recipes/no-street-led.js +8 -5
- package/out/src/shard-recipes/no-street-led.js.map +1 -1
- package/out/src/shard-recipes/no-street.d.ts.map +1 -1
- package/out/src/shard-recipes/no-street.js +1 -1
- package/out/src/shard-recipes/no-street.js.map +1 -1
- package/out/src/shard-recipes/po-box-cedex.d.ts +3 -3
- package/out/src/shard-recipes/po-box-cedex.d.ts.map +1 -1
- package/out/src/shard-recipes/po-box-cedex.js +101 -127
- package/out/src/shard-recipes/po-box-cedex.js.map +1 -1
- package/out/src/shard-recipes/po-box.d.ts.map +1 -1
- package/out/src/shard-recipes/po-box.js +1 -1
- package/out/src/shard-recipes/po-box.js.map +1 -1
- package/out/src/shard-recipes/scaffold.d.ts +58 -9
- package/out/src/shard-recipes/scaffold.d.ts.map +1 -1
- package/out/src/shard-recipes/scaffold.js +87 -29
- package/out/src/shard-recipes/scaffold.js.map +1 -1
- package/out/src/shard-recipes/street-affix.d.ts +54 -0
- package/out/src/shard-recipes/street-affix.d.ts.map +1 -1
- package/out/src/shard-recipes/street-affix.js +253 -111
- package/out/src/shard-recipes/street-affix.js.map +1 -1
- package/out/src/shard-recipes/street-bare.d.ts.map +1 -1
- package/out/src/shard-recipes/street-bare.js +3 -3
- package/out/src/shard-recipes/street-bare.js.map +1 -1
- package/out/src/shard-recipes/street.d.ts.map +1 -1
- package/out/src/shard-recipes/street.js +2 -2
- package/out/src/shard-recipes/street.js.map +1 -1
- package/out/src/shard-recipes/sub-venue-sources.d.ts +228 -0
- package/out/src/shard-recipes/sub-venue-sources.d.ts.map +1 -0
- package/out/src/shard-recipes/sub-venue-sources.js +664 -0
- package/out/src/shard-recipes/sub-venue-sources.js.map +1 -0
- package/out/src/shard-recipes/sub-venue.d.ts +269 -0
- package/out/src/shard-recipes/sub-venue.d.ts.map +1 -0
- package/out/src/shard-recipes/sub-venue.js +709 -0
- package/out/src/shard-recipes/sub-venue.js.map +1 -0
- package/out/src/shard-recipes/unit.d.ts +1 -1
- package/out/src/shard-recipes/unit.d.ts.map +1 -1
- package/out/src/shard-recipes/unit.js +26 -74
- package/out/src/shard-recipes/unit.js.map +1 -1
- package/out/src/{synthesize-anchor-absorption.d.ts → synthesizers/anchor-absorption.d.ts} +1 -1
- package/out/src/synthesizers/anchor-absorption.d.ts.map +1 -0
- package/out/src/{synthesize-anchor-absorption.js → synthesizers/anchor-absorption.js} +1 -1
- package/out/src/synthesizers/anchor-absorption.js.map +1 -0
- package/out/src/{synthesize-boundary-stress.d.ts → synthesizers/boundary-stress.d.ts} +3 -3
- package/out/src/synthesizers/boundary-stress.d.ts.map +1 -0
- package/out/src/{synthesize-boundary-stress.js → synthesizers/boundary-stress.js} +2 -2
- package/out/src/synthesizers/boundary-stress.js.map +1 -0
- package/out/src/{synthesize-german.d.ts → synthesizers/german.d.ts} +2 -2
- package/out/src/synthesizers/german.d.ts.map +1 -0
- package/out/src/{synthesize-german.js → synthesizers/german.js} +2 -2
- package/out/src/synthesizers/german.js.map +1 -0
- package/out/src/{synthesize-house-venue.d.ts → synthesizers/house-venue.d.ts} +4 -4
- package/out/src/synthesizers/house-venue.d.ts.map +1 -0
- package/out/src/{synthesize-house-venue.js → synthesizers/house-venue.js} +4 -4
- package/out/src/synthesizers/house-venue.js.map +1 -0
- package/out/src/{synthesize-intersection.d.ts → synthesizers/intersection.d.ts} +2 -2
- package/out/src/synthesizers/intersection.d.ts.map +1 -0
- package/out/src/{synthesize-intersection.js → synthesizers/intersection.js} +1 -1
- package/out/src/synthesizers/intersection.js.map +1 -0
- package/out/src/{synthesize-no-street.d.ts → synthesizers/no-street.d.ts} +3 -3
- package/out/src/synthesizers/no-street.d.ts.map +1 -0
- package/out/src/{synthesize-no-street.js → synthesizers/no-street.js} +2 -2
- package/out/src/synthesizers/no-street.js.map +1 -0
- package/out/src/{synthesize-po-box.d.ts → synthesizers/po-box.d.ts} +2 -2
- package/out/src/synthesizers/po-box.d.ts.map +1 -0
- package/out/src/{synthesize-po-box.js → synthesizers/po-box.js} +1 -1
- package/out/src/synthesizers/po-box.js.map +1 -0
- package/out/src/{synthesize-street.d.ts → synthesizers/street.d.ts} +2 -2
- package/out/src/synthesizers/street.d.ts.map +1 -0
- package/out/src/{synthesize-street.js → synthesizers/street.js} +2 -2
- package/out/src/synthesizers/street.js.map +1 -0
- package/out/src/{synthesize.d.ts → synthesizers/utils.d.ts} +3 -3
- package/out/src/synthesizers/utils.d.ts.map +1 -0
- package/out/src/{synthesize.js → synthesizers/utils.js} +4 -15
- package/out/src/synthesizers/utils.js.map +1 -0
- package/out/src/tools/align-shard.d.ts.map +1 -1
- package/out/src/tools/align-shard.js +4 -8
- package/out/src/tools/align-shard.js.map +1 -1
- package/out/src/tools/audit.d.ts.map +1 -1
- package/out/src/tools/audit.js +4 -4
- package/out/src/tools/audit.js.map +1 -1
- package/out/src/tools/corpus-stats.d.ts +2 -2
- package/out/src/tools/corpus-stats.d.ts.map +1 -1
- package/out/src/tools/corpus-stats.js +18 -31
- package/out/src/tools/corpus-stats.js.map +1 -1
- package/out/src/tools/fetch/download.d.ts.map +1 -1
- package/out/src/tools/fetch/download.js +5 -6
- package/out/src/tools/fetch/download.js.map +1 -1
- package/out/src/tools/fetch/imls-pls.d.ts.map +1 -1
- package/out/src/tools/fetch/imls-pls.js +3 -15
- package/out/src/tools/fetch/imls-pls.js.map +1 -1
- package/out/src/tools/fetch/index.d.ts +14 -1
- package/out/src/tools/fetch/index.d.ts.map +1 -1
- package/out/src/tools/fetch/index.js +14 -1
- package/out/src/tools/fetch/index.js.map +1 -1
- package/out/src/tools/fetch/nad.d.ts +1 -1
- package/out/src/tools/fetch/nad.js +1 -1
- package/out/src/tools/fetch/nppes.d.ts.map +1 -1
- package/out/src/tools/fetch/nppes.js +8 -14
- package/out/src/tools/fetch/nppes.js.map +1 -1
- package/out/src/tools/fetch/openaddresses.d.ts +1 -1
- package/out/src/tools/fetch/openaddresses.js +1 -1
- package/out/src/tools/fetch/ourairports.d.ts +45 -0
- package/out/src/tools/fetch/ourairports.d.ts.map +1 -0
- package/out/src/tools/fetch/ourairports.js +123 -0
- package/out/src/tools/fetch/ourairports.js.map +1 -0
- package/out/src/tools/fetch/wikidata-subvenue.d.ts +171 -0
- package/out/src/tools/fetch/wikidata-subvenue.d.ts.map +1 -0
- package/out/src/tools/fetch/wikidata-subvenue.js +275 -0
- package/out/src/tools/fetch/wikidata-subvenue.js.map +1 -0
- package/out/src/tools/golden-expand.d.ts.map +1 -1
- package/out/src/tools/golden-expand.js +18 -16
- package/out/src/tools/golden-expand.js.map +1 -1
- package/out/src/tools/golden-promote.d.ts.map +1 -1
- package/out/src/tools/golden-promote.js +12 -6
- package/out/src/tools/golden-promote.js.map +1 -1
- package/out/src/tools/golden-relabel-street.d.ts +196 -0
- package/out/src/tools/golden-relabel-street.d.ts.map +1 -0
- package/out/src/tools/golden-relabel-street.js +440 -0
- package/out/src/tools/golden-relabel-street.js.map +1 -0
- package/out/src/tools/index.d.ts +4 -0
- package/out/src/tools/index.d.ts.map +1 -1
- package/out/src/tools/index.js +4 -0
- package/out/src/tools/index.js.map +1 -1
- package/out/src/tools/jsonl-to-parquet.d.ts.map +1 -1
- package/out/src/tools/jsonl-to-parquet.js +3 -2
- package/out/src/tools/jsonl-to-parquet.js.map +1 -1
- package/out/src/tools/lint-shard-vocab.d.ts.map +1 -1
- package/out/src/tools/lint-shard-vocab.js +3 -2
- package/out/src/tools/lint-shard-vocab.js.map +1 -1
- package/out/src/tools/lint-shard.d.ts +3 -3
- package/out/src/tools/lint-shard.d.ts.map +1 -1
- package/out/src/tools/lint-shard.js +23 -32
- package/out/src/tools/lint-shard.js.map +1 -1
- package/out/src/tools/overlay-manifest.d.ts.map +1 -1
- package/out/src/tools/overlay-manifest.js +2 -1
- package/out/src/tools/overlay-manifest.js.map +1 -1
- package/out/src/tools/overture-subvenue.d.ts +112 -0
- package/out/src/tools/overture-subvenue.d.ts.map +1 -0
- package/out/src/tools/overture-subvenue.js +144 -0
- package/out/src/tools/overture-subvenue.js.map +1 -0
- package/out/src/tools/shard-kryptonite.d.ts +1 -1
- package/out/src/tools/shard-kryptonite.d.ts.map +1 -1
- package/out/src/tools/shard-kryptonite.js +6 -6
- package/out/src/tools/shard-kryptonite.js.map +1 -1
- package/out/src/tools/shard-translit.d.ts +6 -3
- package/out/src/tools/shard-translit.d.ts.map +1 -1
- package/out/src/tools/shard-translit.js +9 -8
- package/out/src/tools/shard-translit.js.map +1 -1
- package/out/src/tools/sub-venue-lexicon.d.ts +507 -0
- package/out/src/tools/sub-venue-lexicon.d.ts.map +1 -0
- package/out/src/tools/sub-venue-lexicon.js +817 -0
- package/out/src/tools/sub-venue-lexicon.js.map +1 -0
- package/out/src/tools/sub-venue-promotions.d.ts +94 -0
- package/out/src/tools/sub-venue-promotions.d.ts.map +1 -0
- package/out/src/tools/sub-venue-promotions.js +266 -0
- package/out/src/tools/sub-venue-promotions.js.map +1 -0
- package/out/src/{align.d.ts → utils/align.d.ts} +1 -1
- package/out/src/utils/align.d.ts.map +1 -0
- package/out/src/utils/align.js.map +1 -0
- package/out/src/{golden.d.ts → utils/golden.d.ts} +5 -0
- package/out/src/utils/golden.d.ts.map +1 -0
- package/out/src/{golden.js → utils/golden.js} +24 -10
- package/out/src/utils/golden.js.map +1 -0
- package/out/src/utils/index.d.ts +14 -0
- package/out/src/utils/index.d.ts.map +1 -0
- package/out/src/utils/index.js +14 -0
- package/out/src/utils/index.js.map +1 -0
- package/out/src/utils/license.d.ts.map +1 -0
- package/out/src/utils/license.js.map +1 -0
- package/out/src/{parquet.d.ts → utils/parquet.d.ts} +2 -2
- package/out/src/utils/parquet.d.ts.map +1 -0
- package/out/src/{parquet.js → utils/parquet.js} +3 -15
- package/out/src/utils/parquet.js.map +1 -0
- package/out/src/{split.d.ts → utils/split.d.ts} +8 -1
- package/out/src/utils/split.d.ts.map +1 -0
- package/out/src/{split.js → utils/split.js} +7 -0
- package/out/src/utils/split.js.map +1 -0
- package/out/src/utils/tokenize.d.ts.map +1 -0
- package/out/src/utils/tokenize.js.map +1 -0
- package/out/src/{wof-json.d.ts → utils/wof-json.d.ts} +2 -2
- package/out/src/utils/wof-json.d.ts.map +1 -0
- package/out/src/{wof-json.js → utils/wof-json.js} +5 -8
- package/out/src/utils/wof-json.js.map +1 -0
- package/package.json +66 -20
- package/src/adapters/ban/adapter.ts +58 -68
- package/src/adapters/ban/street-decompose.ts +17 -25
- package/src/adapters/fcc-bdc/adapter.ts +5 -36
- package/src/adapters/geonames/adapter.ts +67 -76
- package/src/adapters/geonames-postal/adapter.ts +45 -54
- package/src/adapters/gnaf/adapter.ts +7 -11
- package/src/adapters/gnaf/assemble.ts +7 -11
- package/src/adapters/index.ts +3 -2
- package/src/adapters/openaddresses/adapter.ts +6 -12
- package/src/adapters/overture/adapter.ts +6 -10
- package/src/adapters/state-hi-schools/adapter.ts +53 -77
- package/src/adapters/state-ia-contractors/adapter.ts +55 -80
- package/src/adapters/state-ny-notaries/adapter.ts +64 -89
- package/src/adapters/state-tx-notaries/adapter.ts +63 -88
- package/src/adapters/synth-po-box/adapter.ts +12 -23
- package/src/adapters/tiger/adapter.ts +4 -3
- package/src/adapters/tiger/street-decompose.ts +21 -31
- package/src/adapters/usgov-hrsa-fqhc/adapter.ts +64 -88
- package/src/adapters/usgov-imls-pls/adapter.ts +57 -86
- package/src/adapters/usgov-irs-bmf/adapter.ts +55 -66
- package/src/adapters/usgov-nad/adapter.ts +7 -11
- package/src/adapters/usgov-nppes/adapter.ts +54 -79
- package/src/adapters/usgov-samhsa-treatment-locator/adapter.ts +48 -75
- package/src/{adapter.ts → adapters/utils/index.ts} +59 -3
- package/src/adapters/wof-admin-jp/adapter.ts +35 -9
- package/src/adapters/wof-admin-json/adapter.ts +3 -4
- package/src/adapters/wof-postalcode-json/adapter.ts +3 -4
- package/src/build.ts +12 -11
- package/src/index.ts +3 -17
- package/src/name-prone-us-suffixes.ts +12 -0
- package/src/parquet-wrapper/reader.ts +19 -0
- package/src/parquet-wrapper/schema.ts +4 -0
- package/src/runner.ts +7 -2
- package/src/shard-recipes/anchor-absorption.ts +5 -4
- package/src/shard-recipes/boundary-stress.ts +6 -2
- package/src/shard-recipes/country-balanced.ts +77 -91
- package/src/shard-recipes/fr-admin-split.ts +3 -3
- package/src/shard-recipes/fr-fragment.ts +10 -7
- package/src/shard-recipes/fr-lieudit.ts +3 -3
- package/src/shard-recipes/fr-order.ts +18 -83
- package/src/shard-recipes/german.ts +17 -80
- package/src/shard-recipes/house-venue.ts +2 -1
- package/src/shard-recipes/index.ts +4 -1
- package/src/shard-recipes/intersection.ts +37 -74
- package/src/shard-recipes/locale.ts +31 -39
- package/src/shard-recipes/no-fragment.ts +10 -7
- package/src/shard-recipes/no-street-led.ts +10 -7
- package/src/shard-recipes/no-street.ts +2 -1
- package/src/shard-recipes/po-box-cedex.ts +127 -161
- package/src/shard-recipes/po-box.ts +6 -1
- package/src/shard-recipes/scaffold.ts +125 -27
- package/src/shard-recipes/street-affix.ts +335 -134
- package/src/shard-recipes/street-bare.ts +4 -3
- package/src/shard-recipes/street.ts +3 -2
- package/src/shard-recipes/sub-venue-sources.ts +895 -0
- package/src/shard-recipes/sub-venue.ts +982 -0
- package/src/shard-recipes/unit.ts +27 -85
- package/src/{synthesize-boundary-stress.ts → synthesizers/boundary-stress.ts} +2 -2
- package/src/{synthesize-german.ts → synthesizers/german.ts} +2 -2
- package/src/{synthesize-house-venue.ts → synthesizers/house-venue.ts} +4 -4
- package/src/{synthesize-intersection.ts → synthesizers/intersection.ts} +1 -1
- package/src/{synthesize-no-street.ts → synthesizers/no-street.ts} +2 -2
- package/src/{synthesize-po-box.ts → synthesizers/po-box.ts} +1 -1
- package/src/{synthesize-street.ts → synthesizers/street.ts} +2 -2
- package/src/{synthesize.ts → synthesizers/utils.ts} +4 -18
- package/src/tools/align-shard.ts +4 -7
- package/src/tools/audit.ts +6 -5
- package/src/tools/corpus-stats.ts +25 -33
- package/src/tools/fetch/download.ts +7 -5
- package/src/tools/fetch/imls-pls.ts +4 -18
- package/src/tools/fetch/index.ts +14 -1
- package/src/tools/fetch/nad.ts +1 -1
- package/src/tools/fetch/nppes.ts +9 -16
- package/src/tools/fetch/openaddresses.ts +1 -1
- package/src/tools/fetch/ourairports.ts +166 -0
- package/src/tools/fetch/wikidata-subvenue.ts +386 -0
- package/src/tools/golden-expand.ts +18 -13
- package/src/tools/golden-promote.ts +15 -6
- package/src/tools/golden-relabel-street.ts +685 -0
- package/src/tools/index.ts +4 -0
- package/src/tools/jsonl-to-parquet.ts +3 -2
- package/src/tools/lint-shard-vocab.ts +3 -2
- package/src/tools/lint-shard.ts +33 -36
- package/src/tools/overlay-manifest.ts +2 -1
- package/src/tools/overture-subvenue.ts +215 -0
- package/src/tools/shard-kryptonite.ts +8 -9
- package/src/tools/shard-translit.ts +22 -11
- package/src/tools/sub-venue-lexicon.ts +1250 -0
- package/src/tools/sub-venue-promotions.ts +330 -0
- package/src/{align.ts → utils/align.ts} +1 -1
- package/src/{golden.ts → utils/golden.ts} +25 -11
- package/src/utils/index.ts +14 -0
- package/src/{parquet.ts → utils/parquet.ts} +5 -19
- package/src/{split.ts → utils/split.ts} +8 -2
- package/src/{wof-json.ts → utils/wof-json.ts} +5 -8
- package/out/src/adapter.d.ts.map +0 -1
- package/out/src/adapter.js.map +0 -1
- package/out/src/align.d.ts.map +0 -1
- package/out/src/align.js.map +0 -1
- package/out/src/format.d.ts +0 -14
- package/out/src/format.d.ts.map +0 -1
- package/out/src/format.js +0 -14
- package/out/src/format.js.map +0 -1
- package/out/src/golden.d.ts.map +0 -1
- package/out/src/golden.js.map +0 -1
- package/out/src/license.d.ts.map +0 -1
- package/out/src/license.js.map +0 -1
- package/out/src/parquet.d.ts.map +0 -1
- package/out/src/parquet.js.map +0 -1
- package/out/src/split.d.ts.map +0 -1
- package/out/src/split.js.map +0 -1
- package/out/src/synthesize-anchor-absorption.d.ts.map +0 -1
- package/out/src/synthesize-anchor-absorption.js.map +0 -1
- package/out/src/synthesize-boundary-stress.d.ts.map +0 -1
- package/out/src/synthesize-boundary-stress.js.map +0 -1
- package/out/src/synthesize-german.d.ts.map +0 -1
- package/out/src/synthesize-german.js.map +0 -1
- package/out/src/synthesize-house-venue.d.ts.map +0 -1
- package/out/src/synthesize-house-venue.js.map +0 -1
- package/out/src/synthesize-intersection.d.ts.map +0 -1
- package/out/src/synthesize-intersection.js.map +0 -1
- package/out/src/synthesize-no-street.d.ts.map +0 -1
- package/out/src/synthesize-no-street.js.map +0 -1
- package/out/src/synthesize-po-box.d.ts.map +0 -1
- package/out/src/synthesize-po-box.js.map +0 -1
- package/out/src/synthesize-street.d.ts.map +0 -1
- package/out/src/synthesize-street.js.map +0 -1
- package/out/src/synthesize.d.ts.map +0 -1
- package/out/src/synthesize.js.map +0 -1
- package/out/src/tokenize.d.ts.map +0 -1
- package/out/src/tokenize.js.map +0 -1
- package/out/src/wof-json.d.ts.map +0 -1
- package/out/src/wof-json.js.map +0 -1
- package/src/format.ts +0 -14
- /package/out/src/{align.js → utils/align.js} +0 -0
- /package/out/src/{license.d.ts → utils/license.d.ts} +0 -0
- /package/out/src/{license.js → utils/license.js} +0 -0
- /package/out/src/{tokenize.d.ts → utils/tokenize.d.ts} +0 -0
- /package/out/src/{tokenize.js → utils/tokenize.js} +0 -0
- /package/src/{synthesize-anchor-absorption.ts → synthesizers/anchor-absorption.ts} +0 -0
- /package/src/{license.ts → utils/license.ts} +0 -0
- /package/src/{tokenize.ts → utils/tokenize.ts} +0 -0
package/src/tools/fetch/nppes.ts
CHANGED
|
@@ -24,19 +24,16 @@
|
|
|
24
24
|
/* oxlint-disable sister-software/prefer-region-over-marks -- these markers label steps inside one
|
|
25
25
|
procedure, not sections of declarations. A region there folds nothing a reader wants folded. */
|
|
26
26
|
|
|
27
|
-
import { execFile } from "node:child_process"
|
|
28
27
|
import { existsSync, mkdirSync, statSync } from "node:fs"
|
|
29
28
|
import { rm } from "node:fs/promises"
|
|
30
|
-
import { join } from "node:path"
|
|
31
|
-
import { promisify } from "node:util"
|
|
29
|
+
import { basename, join } from "node:path"
|
|
32
30
|
|
|
31
|
+
import { extractZipEntry, listZipEntries } from "@mailwoman/core/fs/zip"
|
|
33
32
|
import { sha256File } from "@mailwoman/core/utils"
|
|
34
33
|
|
|
35
34
|
import type { BaseFetchOptions, FetchSummary } from "./download.ts"
|
|
36
35
|
import { downloadToFile, readManifest, writeManifest } from "./download.ts"
|
|
37
36
|
|
|
38
|
-
const execFileAsync = promisify(execFile)
|
|
39
|
-
|
|
40
37
|
const INDEX_URL = "https://download.cms.gov/nppes/NPI_Files.html"
|
|
41
38
|
const BASE_URL = "https://download.cms.gov/nppes"
|
|
42
39
|
const SLUG = "usgov-nppes"
|
|
@@ -74,18 +71,13 @@ async function discoverLatestZip(): Promise<string | undefined> {
|
|
|
74
71
|
}
|
|
75
72
|
|
|
76
73
|
/**
|
|
77
|
-
*
|
|
74
|
+
* The main registry CSV (npidata_pfile_*.csv), which the archive also carries alongside a header file and a per-month
|
|
75
|
+
* change file.
|
|
78
76
|
*/
|
|
79
77
|
async function findNpidataCSV(zipPath: string): Promise<string | undefined> {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
for (const line of listing.stdout.split("\n")) {
|
|
83
|
-
const match = /npidata_pfile\S+\.csv/i.exec(line)
|
|
84
|
-
|
|
85
|
-
if (match?.[0]) return match[0]
|
|
86
|
-
}
|
|
78
|
+
const entries = await listZipEntries(zipPath)
|
|
87
79
|
|
|
88
|
-
return
|
|
80
|
+
return entries.find((entry) => /npidata_pfile\S+\.csv/i.test(entry.name))?.name
|
|
89
81
|
}
|
|
90
82
|
|
|
91
83
|
export async function fetchNPPES(options: FetchNPPESOptions, report?: (line: string) => void): Promise<FetchSummary> {
|
|
@@ -148,9 +140,10 @@ export async function fetchNPPES(options: FetchNPPESOptions, report?: (line: str
|
|
|
148
140
|
}
|
|
149
141
|
|
|
150
142
|
report?.(` Extracting: ${csvName}`)
|
|
151
|
-
await execFileAsync("unzip", ["-o", "-j", zipDest, csvName, "-d", destDir])
|
|
152
143
|
|
|
153
|
-
const csvDest = join(destDir, csvName)
|
|
144
|
+
const csvDest = join(destDir, basename(csvName))
|
|
145
|
+
|
|
146
|
+
await extractZipEntry(zipDest, csvName, csvDest)
|
|
154
147
|
const csvSize = statSync(csvDest).size
|
|
155
148
|
const csvSha = await sha256File(csvDest)
|
|
156
149
|
report?.(` CSV size: ${(csvSize / 1024 / 1024).toFixed(1)} MB`)
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* ```sh
|
|
45
45
|
* # With token (preferred). Default country: ca. Supports any OA country code (us-west, fr, …)
|
|
46
46
|
* OA_BATCH_TOKEN=<token> mailwoman corpus fetch openaddresses --country ca \
|
|
47
|
-
* --out-root /
|
|
47
|
+
* --out-root $MAILWOMAN_DATA_ROOT/corpus/sources
|
|
48
48
|
*
|
|
49
49
|
* # Without token (will detect + print instructions, then report the failure):
|
|
50
50
|
* mailwoman corpus fetch openaddresses --country ca
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Fetch the OurAirports CSV dumps — the VENUE side of the sub-venue corpus arc (#35).
|
|
7
|
+
*
|
|
8
|
+
* Source : https://davidmegginson.github.io/ourairports-data/ (the project's own GitHub Pages
|
|
9
|
+
* mirror of the nightly export; `ourairports.com/data/` redirects here).
|
|
10
|
+
* License: PUBLIC DOMAIN. OurAirports places its data in the public domain and asks only for a
|
|
11
|
+
* courtesy credit — no attribution obligation rides on a derived shard, which makes this
|
|
12
|
+
* the one transport source in the arc with no licensing question at all. Tier A.
|
|
13
|
+
*
|
|
14
|
+
* ## What it is good for, and what it is not
|
|
15
|
+
*
|
|
16
|
+
* `airports.csv` is every airport on earth with ICAO/IATA codes, coordinates, `municipality`, and
|
|
17
|
+
* `iso_country` — 12.7 MB, ~83,000 rows as of 2026-08-04. That is the CONTAINING VENUE for a
|
|
18
|
+
* `<sub-venue>, <venue>, <street>, <locality>, <postcode>` corpus line, and it is better at that job
|
|
19
|
+
* than OSM: every row is named, the name is canonical, and `municipality` gives the locality without
|
|
20
|
+
* a spatial join.
|
|
21
|
+
*
|
|
22
|
+
* It carries NO interior structure. There is no terminal, concourse, gate or pier table — the
|
|
23
|
+
* corpus task says as much ("Good for the venue side of each pair, weaker on interior structure")
|
|
24
|
+
* and a row-level read confirms it. Pair it with the OSM `aeroway` extractor
|
|
25
|
+
* (`@mailwoman/osm/sdk`'s `extractOSMSubVenues`), which is where the sub-venue half comes from.
|
|
26
|
+
*
|
|
27
|
+
* ## Why `downloadToFile` and not `APIClient`
|
|
28
|
+
*
|
|
29
|
+
* `AGENTS.md` routes HTTP through `APIClient`, and that rule is about API REQUESTS — small bodies,
|
|
30
|
+
* repeated calls, rate-limited hosts. This is four static file transfers against a GitHub Pages CDN
|
|
31
|
+
* with no rate limit and nothing to pace, run once per refresh. It uses the same `downloadToFile`
|
|
32
|
+
* every other module in this `fetch/` family uses, which is where the retry and timeout live.
|
|
33
|
+
* The Wikidata sibling (`wikidata-subvenue.ts`) IS an API client and is built on `APIClient`
|
|
34
|
+
* accordingly; the split between the two is the one `AGENTS.md` draws.
|
|
35
|
+
*
|
|
36
|
+
* Invoke via `mailwoman corpus fetch ourairports --out-root <path>`.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { mkdirSync } from "node:fs"
|
|
40
|
+
import { join } from "node:path"
|
|
41
|
+
|
|
42
|
+
import { sha256File } from "@mailwoman/core/utils"
|
|
43
|
+
|
|
44
|
+
import type { BaseFetchOptions, FetchSummary } from "./download.ts"
|
|
45
|
+
import { downloadToFile, writeManifest } from "./download.ts"
|
|
46
|
+
|
|
47
|
+
const SLUG = "ourairports"
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The GitHub Pages mirror the project itself publishes. `ourairports.com/data/*.csv` 302s here, so pointing at the
|
|
51
|
+
* mirror directly saves a redirect and is the URL the project's own README gives.
|
|
52
|
+
*/
|
|
53
|
+
const BASE_URL = "https://davidmegginson.github.io/ourairports-data"
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The files worth having, and why each one.
|
|
57
|
+
*
|
|
58
|
+
* `airports.csv` is the payload. The other three are small joins that turn its codes into text: `countries.csv` and
|
|
59
|
+
* `regions.csv` expand `iso_country`/`iso_region` into names (a corpus line needs "Germany", not "DE"), and
|
|
60
|
+
* `runways.csv` is the only file carrying per-airport sub-structure of any kind — runway designators, which are NOT
|
|
61
|
+
* sub-venue designators (nobody addresses mail to a runway) but are worth having on disk as the negative class if the
|
|
62
|
+
* shard ever needs one.
|
|
63
|
+
*/
|
|
64
|
+
const FILES = ["airports.csv", "countries.csv", "regions.csv", "runways.csv"] as const
|
|
65
|
+
|
|
66
|
+
export type FetchOurAirportsOptions = BaseFetchOptions
|
|
67
|
+
|
|
68
|
+
interface OurAirportsFileEntry {
|
|
69
|
+
filename: string
|
|
70
|
+
source_url: string
|
|
71
|
+
sha256: string
|
|
72
|
+
bytes: number
|
|
73
|
+
/**
|
|
74
|
+
* The upstream `Last-Modified`, when the CDN gave one. This is the DATA's vintage; `downloaded_at` is only when we
|
|
75
|
+
* asked. `corpus/AGENTS.md` has the standing warning that a file's mtime is not its data's vintage — recording the
|
|
76
|
+
* upstream header is how a later refresh decision gets made on the right number.
|
|
77
|
+
*/
|
|
78
|
+
last_modified: string | null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface OurAirportsManifest {
|
|
82
|
+
source: string
|
|
83
|
+
base_url: string
|
|
84
|
+
license: string
|
|
85
|
+
downloaded_at: string
|
|
86
|
+
files: OurAirportsFileEntry[]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Read the upstream `Last-Modified` with a HEAD. Returns `null` on any failure — provenance metadata is nice to have
|
|
91
|
+
* and must never fail a download that otherwise succeeded.
|
|
92
|
+
*/
|
|
93
|
+
async function readLastModified(url: string): Promise<string | null> {
|
|
94
|
+
try {
|
|
95
|
+
const res = await fetch(url, { method: "HEAD", signal: AbortSignal.timeout(30_000) })
|
|
96
|
+
|
|
97
|
+
return res.ok ? res.headers.get("last-modified") : null
|
|
98
|
+
} catch {
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Download the OurAirports CSVs into `<outRoot>/ourairports/`, with a sibling `MANIFEST.json` carrying each file's
|
|
105
|
+
* origin URL, sha256, byte count and upstream `Last-Modified`.
|
|
106
|
+
*/
|
|
107
|
+
export async function fetchOurAirports(
|
|
108
|
+
options: FetchOurAirportsOptions,
|
|
109
|
+
report?: (line: string) => void
|
|
110
|
+
): Promise<FetchSummary> {
|
|
111
|
+
const destDir = join(options.outRoot, SLUG)
|
|
112
|
+
mkdirSync(destDir, { recursive: true })
|
|
113
|
+
|
|
114
|
+
const entries: OurAirportsFileEntry[] = []
|
|
115
|
+
const failedCodes: string[] = []
|
|
116
|
+
let fetched = 0
|
|
117
|
+
let failed = 0
|
|
118
|
+
|
|
119
|
+
for (const filename of FILES) {
|
|
120
|
+
const url = `${BASE_URL}/${filename}`
|
|
121
|
+
const dest = join(destDir, filename)
|
|
122
|
+
|
|
123
|
+
report?.(`=== ${SLUG} / ${filename}`)
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const [{ bytes }, lastModified] = await Promise.all([
|
|
127
|
+
downloadToFile({
|
|
128
|
+
url,
|
|
129
|
+
dest,
|
|
130
|
+
timeoutMs: 600_000,
|
|
131
|
+
retries: 2,
|
|
132
|
+
headers: { "Accept-Encoding": "gzip, br" },
|
|
133
|
+
report,
|
|
134
|
+
}),
|
|
135
|
+
readLastModified(url),
|
|
136
|
+
])
|
|
137
|
+
|
|
138
|
+
entries.push({
|
|
139
|
+
filename,
|
|
140
|
+
source_url: url,
|
|
141
|
+
sha256: await sha256File(dest),
|
|
142
|
+
bytes,
|
|
143
|
+
last_modified: lastModified,
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
fetched++
|
|
147
|
+
} catch (error) {
|
|
148
|
+
report?.(`✗ ${filename}: ${error instanceof Error ? error.message : String(error)}`)
|
|
149
|
+
failedCodes.push(filename)
|
|
150
|
+
|
|
151
|
+
failed++
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const manifest: OurAirportsManifest = {
|
|
156
|
+
source: "OurAirports",
|
|
157
|
+
base_url: BASE_URL,
|
|
158
|
+
license: "public domain (courtesy credit requested)",
|
|
159
|
+
downloaded_at: new Date().toISOString(),
|
|
160
|
+
files: entries,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
await writeManifest(join(destDir, "MANIFEST.json"), manifest)
|
|
164
|
+
|
|
165
|
+
return { fetched, skipped: 0, failed, failedCodes }
|
|
166
|
+
}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Fetch the multilingual sub-venue designator vocabulary from Wikidata (#35 wave 1).
|
|
7
|
+
*
|
|
8
|
+
* Source : https://query.wikidata.org/sparql (the Wikidata Query Service).
|
|
9
|
+
* License: CC0. Wikidata's data is public-domain dedicated, so nothing rides on a derived shard.
|
|
10
|
+
* Tier A.
|
|
11
|
+
*
|
|
12
|
+
* ## The pull is CLASS labels, not instance names — and that inversion is the whole design
|
|
13
|
+
*
|
|
14
|
+
* The obvious read of "Wikidata for localized designators" is: fetch every airport terminal entity
|
|
15
|
+
* and read its name in each language. That was tried first and it is the WRONG query. Wikidata
|
|
16
|
+
* knows 246 items that are `instance of / subclass of*` airport terminal, carrying 775 labels
|
|
17
|
+
* between them (measured 2026-08-04), and most of those labels are proper names that translate
|
|
18
|
+
* verbatim — `TWA Flight Center` is spelled `TWA Flight Center` in fifteen languages. The
|
|
19
|
+
* instance layer is thin and its localization is mostly a no-op.
|
|
20
|
+
*
|
|
21
|
+
* The DESIGNATOR is the label of the CLASS. `wd:Q849706` ("airport terminal") is labelled `Terminal`
|
|
22
|
+
* in German, `terminal aéroportuaire` in French, `ターミナルビル` in Japanese, `航站楼` in Chinese —
|
|
23
|
+
* and `skos:altLabel` adds the aliases (`Abfertigungsgebäude`, `Flughafenterminal`, `aerostazione`).
|
|
24
|
+
* Eight concept ids yield 877 label+alias rows across 174 languages, which is the vocabulary the
|
|
25
|
+
* corpus task asked for and the instance query does not contain. {@link SUBVENUE_CONCEPTS} is that
|
|
26
|
+
* list of ids; {@link buildDesignatorLabelQuery} is that query.
|
|
27
|
+
*
|
|
28
|
+
* Instance labels are fetched too ({@link buildTerminalInstanceQuery}), for a different job: they
|
|
29
|
+
* are ATTESTED USAGE — evidence of how a designator combines with a modifier or an identifier in
|
|
30
|
+
* running text. 775 rows is small, and it is a validation set, not a vocabulary.
|
|
31
|
+
*
|
|
32
|
+
* ## What a caller must NOT do with the output
|
|
33
|
+
*
|
|
34
|
+
* A class label is a CONCEPT NAME, not a designator as written in an address. Q849706's Spanish
|
|
35
|
+
* label is `terminal aeroportuaria` and its French is `terminal d'aéroport`; nobody writes either on
|
|
36
|
+
* an envelope, they write `Terminal`. Q247739's Spanish is `puerta de embarque` where the addressed
|
|
37
|
+
* form is `Puerta`. So this fetch produces CANDIDATE SURFACES that need a head-noun/curation pass
|
|
38
|
+
* before any of them reaches `neural/venue-structure.ts`'s designator vocabulary — the lexicon
|
|
39
|
+
* builder marks every one `curated: false` and the burden of promotion is on a human. Wiring the raw
|
|
40
|
+
* pull straight into the span proposer would admit multi-word phrases that match nothing and, worse,
|
|
41
|
+
* admit `hall` in a language where it names an ordinary room.
|
|
42
|
+
*
|
|
43
|
+
* ## Why `APIClient` here when the OurAirports sibling uses `downloadToFile`
|
|
44
|
+
*
|
|
45
|
+
* This is the API-request side of `AGENTS.md`'s split: small JSON bodies, several calls per run, and
|
|
46
|
+
* a host that publishes a rate policy and enforces it with 429s. Pacing, bounded `Retry-After`-aware
|
|
47
|
+
* retry, response caching and `ResourceError` mapping all earn their keep, so it extends
|
|
48
|
+
* {@link APIClient}. `ourairports.ts` is four static file transfers off a CDN and correctly does not.
|
|
49
|
+
*
|
|
50
|
+
* WDQS also REQUIRES a descriptive `User-Agent` naming the tool and a contact — an anonymous or
|
|
51
|
+
* library-default agent is blocked outright by the Wikimedia user-agent policy. See
|
|
52
|
+
* {@link WIKIDATA_USER_AGENT}.
|
|
53
|
+
*
|
|
54
|
+
* Invoke via `mailwoman corpus fetch wikidata-subvenue --out-root <path>`.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
import { mkdirSync } from "node:fs"
|
|
58
|
+
import { writeFile } from "node:fs/promises"
|
|
59
|
+
import { join } from "node:path"
|
|
60
|
+
|
|
61
|
+
import { APIClient, type ClockLike } from "@mailwoman/core/api"
|
|
62
|
+
import { buildDiskStorage } from "@mailwoman/core/api/disk-storage"
|
|
63
|
+
import { sha256File } from "@mailwoman/core/utils"
|
|
64
|
+
|
|
65
|
+
import type { BaseFetchOptions, FetchSummary } from "./download.ts"
|
|
66
|
+
import { writeManifest } from "./download.ts"
|
|
67
|
+
|
|
68
|
+
const SLUG = "wikidata-subvenue"
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The SPARQL endpoint. Public, no credential.
|
|
72
|
+
*/
|
|
73
|
+
export const WDQS_ENDPOINT = "https://query.wikidata.org/sparql"
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The `User-Agent` every request carries.
|
|
77
|
+
*
|
|
78
|
+
* NOT decoration. The Wikimedia user-agent policy blocks requests whose agent is absent, generic, or a library default,
|
|
79
|
+
* and WDQS enforces it — an unidentified client gets a 403 that no amount of retrying fixes. The policy asks for a tool
|
|
80
|
+
* name, a URL, and a contact address, all three of which are here.
|
|
81
|
+
*/
|
|
82
|
+
export const WIKIDATA_USER_AGENT =
|
|
83
|
+
"mailwoman/1.0 (https://github.com/sister-software/mailwoman; teffen@sister.software) corpus-subvenue-fetch"
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Minimum spacing between dispatches, in milliseconds.
|
|
87
|
+
*
|
|
88
|
+
* WDQS's published limit is expressed as processing-time budget rather than a request rate, and this run issues fewer
|
|
89
|
+
* than a dozen queries total, so the number is chosen for politeness rather than to sit against a ceiling: one query
|
|
90
|
+
* per second is far inside anything WDQS objects to, and at this volume the whole fetch still completes in seconds.
|
|
91
|
+
*
|
|
92
|
+
* Set as `minRequestIntervalMs` rather than `requestsPerMinute` deliberately — `AGENTS.md` records that
|
|
93
|
+
* `requestsPerMinute` is a BUDGET model whose cooldown lets N requests go out back to back, so it does not deliver N
|
|
94
|
+
* per minute and is not the gate that holds a rate. The interval is.
|
|
95
|
+
*/
|
|
96
|
+
const WDQS_MIN_REQUEST_INTERVAL_MS = 1000
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Per-attempt socket-inactivity timeout. WDQS's own query timeout is 60 seconds and it answers with a 500 when a query
|
|
100
|
+
* exceeds it, so a client timeout below that would turn a server-side timeout into a client-side one and lose the error
|
|
101
|
+
* body that says which query was too expensive.
|
|
102
|
+
*/
|
|
103
|
+
const WDQS_REQUEST_TIMEOUT_MS = 90_000
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Total attempts (including the first) before giving up on a 429/5xx.
|
|
107
|
+
*/
|
|
108
|
+
const WDQS_MAX_ATTEMPTS = 3
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* How long a cached SPARQL response stays fresh. A week: the class labels this pulls change on the timescale at which
|
|
112
|
+
* someone edits a Wikidata concept's German alias, which is to say rarely, and a re-run inside a working session should
|
|
113
|
+
* not re-ask.
|
|
114
|
+
*/
|
|
115
|
+
const WDQS_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* One Wikidata concept whose labels are a designator's multilingual surface set.
|
|
119
|
+
*
|
|
120
|
+
* `designatorID` is the mailwoman-side vocabulary term, matching `neural/venue-structure.ts`'s
|
|
121
|
+
* `VENUE_STRUCTURE_DESIGNATORS` wherever the two overlap. `qid` was resolved by `wbsearchentities` and hand-checked
|
|
122
|
+
* against the entity's English description (recorded below) on 2026-08-04 — a QID picked by search alone is how you end
|
|
123
|
+
* up pulling the labels of a Bronx neighbourhood called Concourse.
|
|
124
|
+
*
|
|
125
|
+
* `wing` is ABSENT and that is a finding, not an oversight: Wikidata has no clean concept for "wing of a building".
|
|
126
|
+
* `wbsearchentities` for "wing" returns a surname, two English villages, a rugby position and a drone company. Since
|
|
127
|
+
* `wing` is the single most valuable designator in the arc — `West Wing` is the one modifier case that already parses,
|
|
128
|
+
* and `East Wing` is the one that does not — its localized surfaces have to come from somewhere else. See the wave-1
|
|
129
|
+
* report.
|
|
130
|
+
*/
|
|
131
|
+
export interface SubVenueConcept {
|
|
132
|
+
designatorID: string
|
|
133
|
+
qid: string
|
|
134
|
+
/**
|
|
135
|
+
* The entity's English description, recorded so a future reader can tell at a glance whether the QID still names what
|
|
136
|
+
* we think it names.
|
|
137
|
+
*/
|
|
138
|
+
gloss: string
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The concept table. Eight ids, each verified against its English description on 2026-08-04.
|
|
143
|
+
*/
|
|
144
|
+
export const SUBVENUE_CONCEPTS: readonly SubVenueConcept[] = [
|
|
145
|
+
{ designatorID: "terminal", qid: "Q849706", gloss: "airport terminal — part of an airport" },
|
|
146
|
+
{ designatorID: "gate", qid: "Q247739", gloss: "gate — airport facility for passenger loading/unloading" },
|
|
147
|
+
{ designatorID: "concourse", qid: "Q862212", gloss: "concourse — place where pathways or roads meet" },
|
|
148
|
+
{ designatorID: "campus", qid: "Q209465", gloss: "campus — cluster of buildings used by an educational institution" },
|
|
149
|
+
{ designatorID: "building", qid: "Q41176", gloss: "building — structure with a roof and walls" },
|
|
150
|
+
{ designatorID: "arcade", qid: "Q186637", gloss: "arcade — covered walk enclosed by a line of arches" },
|
|
151
|
+
{ designatorID: "hall", qid: "Q240854", gloss: "hall — large enclosed room" },
|
|
152
|
+
{ designatorID: "satellite", qid: "Q15990706", gloss: "satellite terminal — detached airport building" },
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The class whose instances are fetched for attested-usage evidence: airport terminal.
|
|
157
|
+
*/
|
|
158
|
+
const TERMINAL_CLASS_QID = "Q849706"
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Build the class-label query: `rdfs:label` and `skos:altLabel` for every concept, in every language, tagged with which
|
|
162
|
+
* of the two it came from so the lexicon can rank a label above an alias.
|
|
163
|
+
*
|
|
164
|
+
* `VALUES` rather than a property path over the whole class tree — the concept list is closed and hand-verified, and a
|
|
165
|
+
* `wdt:P279*` walk from `building` would drag in every structure type on earth.
|
|
166
|
+
*/
|
|
167
|
+
export function buildDesignatorLabelQuery(concepts: readonly SubVenueConcept[] = SUBVENUE_CONCEPTS): string {
|
|
168
|
+
const values = concepts.map((c) => `wd:${c.qid}`).join(" ")
|
|
169
|
+
|
|
170
|
+
return `SELECT ?item ?lang ?label ?kind WHERE {
|
|
171
|
+
VALUES ?item { ${values} }
|
|
172
|
+
{ ?item rdfs:label ?label . BIND("label" AS ?kind) }
|
|
173
|
+
UNION
|
|
174
|
+
{ ?item skos:altLabel ?label . BIND("alt" AS ?kind) }
|
|
175
|
+
BIND(LANG(?label) AS ?lang)
|
|
176
|
+
}`
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Build the instance-label query — every item that is an `instance of` (through any `subclass of` chain) an airport
|
|
181
|
+
* terminal, with all of its labels. Measured at 246 items / 775 labels on 2026-08-04, well inside WDQS's 60-second
|
|
182
|
+
* budget.
|
|
183
|
+
*
|
|
184
|
+
* A caveat worth knowing before trusting a row: Wikidata's P31 on these is not clean. `Q1322696` (Kigali International
|
|
185
|
+
* Airport) is typed as an airport terminal, so the result set mixes AIRPORTS in with terminals. The consumer filters;
|
|
186
|
+
* this module fetches what the query returns.
|
|
187
|
+
*/
|
|
188
|
+
export function buildTerminalInstanceQuery(classQID: string = TERMINAL_CLASS_QID): string {
|
|
189
|
+
return `SELECT ?item ?lang ?label WHERE {
|
|
190
|
+
?item wdt:P31/wdt:P279* wd:${classQID} .
|
|
191
|
+
?item rdfs:label ?label .
|
|
192
|
+
BIND(LANG(?label) AS ?lang)
|
|
193
|
+
}`
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The SPARQL JSON results shape, narrowed to the two column types these queries produce.
|
|
198
|
+
*/
|
|
199
|
+
export interface SPARQLResults {
|
|
200
|
+
results: {
|
|
201
|
+
bindings: Array<Record<string, { type: string; value: string; "xml:lang"?: string }>>
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Whether a decoded body is a SPARQL results envelope. Used as the cache's write validator so an HTML error page served
|
|
207
|
+
* under a 200 is never persisted for the next run to destructure into `undefined`.
|
|
208
|
+
*/
|
|
209
|
+
export function isSPARQLResults(value: unknown): value is SPARQLResults {
|
|
210
|
+
return typeof value === "object" && value !== null && Array.isArray((value as SPARQLResults).results?.bindings)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface CreateWikidataClientOptions {
|
|
214
|
+
/**
|
|
215
|
+
* On-disk response-cache root. Defaults to a `http-cache` directory beside the fetch output.
|
|
216
|
+
*/
|
|
217
|
+
cacheDir: string
|
|
218
|
+
/**
|
|
219
|
+
* Time source powering the pacer and the retry backoff. Defaults to the system clock; tests inject a fake so no suite
|
|
220
|
+
* ever sleeps a real second.
|
|
221
|
+
*/
|
|
222
|
+
clock?: ClockLike
|
|
223
|
+
/**
|
|
224
|
+
* Axios overrides, merged over this client's defaults. THE TEST SEAM — pass an `adapter` and no live call is made.
|
|
225
|
+
* Overriding `headers` wholesale would drop the required `User-Agent`, so don't.
|
|
226
|
+
*/
|
|
227
|
+
axios?: ConstructorParameters<typeof APIClient>[0]["axios"]
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* A Wikidata Query Service client: paced, retrying, disk-cached, and correctly identified.
|
|
232
|
+
*/
|
|
233
|
+
export class WikidataClient extends APIClient {
|
|
234
|
+
/**
|
|
235
|
+
* Run one SPARQL query and return its results envelope.
|
|
236
|
+
*/
|
|
237
|
+
public async query(sparql: string): Promise<SPARQLResults> {
|
|
238
|
+
const url = new URL(WDQS_ENDPOINT)
|
|
239
|
+
url.searchParams.set("query", sparql)
|
|
240
|
+
|
|
241
|
+
const response = await this.fetch<SPARQLResults>({ url: url.toString() })
|
|
242
|
+
|
|
243
|
+
return response.data
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Construct a {@link WikidataClient} with every default resolved.
|
|
249
|
+
*/
|
|
250
|
+
export function createWikidataClient(options: CreateWikidataClientOptions): WikidataClient {
|
|
251
|
+
return new WikidataClient({
|
|
252
|
+
displayName: "Wikidata Query Service",
|
|
253
|
+
minRequestIntervalMs: WDQS_MIN_REQUEST_INTERVAL_MS,
|
|
254
|
+
retry: { maxAttempts: WDQS_MAX_ATTEMPTS },
|
|
255
|
+
clock: options.clock,
|
|
256
|
+
caching: {
|
|
257
|
+
storage: buildDiskStorage({
|
|
258
|
+
directory: options.cacheDir,
|
|
259
|
+
// `value.data` is the cached RESPONSE; `value.data.data` is its body. Passing the response here
|
|
260
|
+
// instead of the body is a silent-failure trap — the predicate returns false for every entry and
|
|
261
|
+
// every run re-fetches while logging "rejected by the configured validate() predicate". Caught
|
|
262
|
+
// on the first live run, 2026-08-04.
|
|
263
|
+
validate: (value) => isSPARQLResults(value.data?.data),
|
|
264
|
+
}),
|
|
265
|
+
ttl: WDQS_CACHE_TTL_MS,
|
|
266
|
+
// The TTL is chosen against Wikidata's edit cadence; letting a CDN header override it would
|
|
267
|
+
// silently replace that reasoning with whatever varnish in front of WDQS happens to send.
|
|
268
|
+
interpretHeader: false,
|
|
269
|
+
},
|
|
270
|
+
axios: {
|
|
271
|
+
headers: {
|
|
272
|
+
"User-Agent": WIKIDATA_USER_AGENT,
|
|
273
|
+
Accept: "application/sparql-results+json",
|
|
274
|
+
},
|
|
275
|
+
timeout: WDQS_REQUEST_TIMEOUT_MS,
|
|
276
|
+
responseType: "json",
|
|
277
|
+
// Axios hands back the RAW STRING when a body fails to parse unless this is off. WDQS serves an
|
|
278
|
+
// HTML error page under some failures, and returning that as `SPARQLResults` would surface as an
|
|
279
|
+
// `undefined` destructure far from the cause.
|
|
280
|
+
transitional: { silentJSONParsing: false },
|
|
281
|
+
...options.axios,
|
|
282
|
+
},
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export type FetchWikidataSubVenueOptions = BaseFetchOptions
|
|
287
|
+
|
|
288
|
+
interface WikidataFileEntry {
|
|
289
|
+
filename: string
|
|
290
|
+
query: string
|
|
291
|
+
rows: number
|
|
292
|
+
sha256: string
|
|
293
|
+
bytes: number
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
interface WikidataManifest {
|
|
297
|
+
source: string
|
|
298
|
+
endpoint: string
|
|
299
|
+
license: string
|
|
300
|
+
user_agent: string
|
|
301
|
+
downloaded_at: string
|
|
302
|
+
concepts: readonly SubVenueConcept[]
|
|
303
|
+
files: WikidataFileEntry[]
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Write a JSON payload and return its manifest entry.
|
|
308
|
+
*/
|
|
309
|
+
async function writePayload(
|
|
310
|
+
destDir: string,
|
|
311
|
+
filename: string,
|
|
312
|
+
query: string,
|
|
313
|
+
results: SPARQLResults
|
|
314
|
+
): Promise<WikidataFileEntry> {
|
|
315
|
+
const path = join(destDir, filename)
|
|
316
|
+
const body = JSON.stringify(results, null, 2) + "\n"
|
|
317
|
+
await writeFile(path, body)
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
filename,
|
|
321
|
+
query,
|
|
322
|
+
rows: results.results.bindings.length,
|
|
323
|
+
sha256: await sha256File(path),
|
|
324
|
+
bytes: Buffer.byteLength(body),
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Run both queries and write their raw SPARQL JSON into `<outRoot>/wikidata-subvenue/`, with a `MANIFEST.json` carrying
|
|
330
|
+
* the endpoint, the exact queries, the concept table, row counts and sha256s.
|
|
331
|
+
*
|
|
332
|
+
* The RAW envelope is written rather than a reshaped one on purpose: the lexicon build is a separate, pure step
|
|
333
|
+
* (`sub-venue-lexicon.ts`) and keeping the fetch output byte-faithful to what WDQS served means a lexicon regeneration
|
|
334
|
+
* never needs the network.
|
|
335
|
+
*/
|
|
336
|
+
export async function fetchWikidataSubVenue(
|
|
337
|
+
options: FetchWikidataSubVenueOptions,
|
|
338
|
+
report?: (line: string) => void
|
|
339
|
+
): Promise<FetchSummary> {
|
|
340
|
+
const destDir = join(options.outRoot, SLUG)
|
|
341
|
+
mkdirSync(destDir, { recursive: true })
|
|
342
|
+
|
|
343
|
+
await using client = createWikidataClient({ cacheDir: join(destDir, "http-cache") })
|
|
344
|
+
|
|
345
|
+
const jobs: Array<{ filename: string; query: string }> = [
|
|
346
|
+
{ filename: "designator-labels.json", query: buildDesignatorLabelQuery() },
|
|
347
|
+
{ filename: "terminal-instance-labels.json", query: buildTerminalInstanceQuery() },
|
|
348
|
+
]
|
|
349
|
+
|
|
350
|
+
const files: WikidataFileEntry[] = []
|
|
351
|
+
const failedCodes: string[] = []
|
|
352
|
+
let fetched = 0
|
|
353
|
+
let failed = 0
|
|
354
|
+
|
|
355
|
+
for (const job of jobs) {
|
|
356
|
+
report?.(`=== ${SLUG} / ${job.filename}`)
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
const results = await client.query(job.query)
|
|
360
|
+
const entry = await writePayload(destDir, job.filename, job.query, results)
|
|
361
|
+
report?.(` ${entry.rows} rows, ${entry.bytes} bytes`)
|
|
362
|
+
files.push(entry)
|
|
363
|
+
|
|
364
|
+
fetched++
|
|
365
|
+
} catch (error) {
|
|
366
|
+
report?.(`✗ ${job.filename}: ${error instanceof Error ? error.message : String(error)}`)
|
|
367
|
+
failedCodes.push(job.filename)
|
|
368
|
+
|
|
369
|
+
failed++
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const manifest: WikidataManifest = {
|
|
374
|
+
source: "Wikidata Query Service",
|
|
375
|
+
endpoint: WDQS_ENDPOINT,
|
|
376
|
+
license: "CC0",
|
|
377
|
+
user_agent: WIKIDATA_USER_AGENT,
|
|
378
|
+
downloaded_at: new Date().toISOString(),
|
|
379
|
+
concepts: SUBVENUE_CONCEPTS,
|
|
380
|
+
files,
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
await writeManifest(join(destDir, "MANIFEST.json"), manifest)
|
|
384
|
+
|
|
385
|
+
return { fetched, skipped: 0, failed, failedCodes }
|
|
386
|
+
}
|
|
@@ -51,7 +51,9 @@ import { dirname } from "node:path"
|
|
|
51
51
|
|
|
52
52
|
import { ParquetReader } from "@dsnp/parquetjs"
|
|
53
53
|
import { $private } from "@mailwoman/core/env"
|
|
54
|
-
import {
|
|
54
|
+
import { tryParsingJSON } from "@mailwoman/core/objects"
|
|
55
|
+
import { dataRootPath } from "@mailwoman/core/utils"
|
|
56
|
+
import { createNewlineWriter } from "spliterator"
|
|
55
57
|
|
|
56
58
|
// ── Types ─────────────────────────────────────────────────────────────────
|
|
57
59
|
|
|
@@ -406,21 +408,17 @@ function parseCandidates(text: string): Candidate[] {
|
|
|
406
408
|
// Strip markdown fences the model sometimes wraps around JSON
|
|
407
409
|
const cleaned = text.replaceAll(/^```(?:json)?\n?|\n?```$/g, "").trim()
|
|
408
410
|
|
|
409
|
-
|
|
410
|
-
const parsed = JSON.parse(cleaned) as unknown
|
|
411
|
+
const parsed = tryParsingJSON(cleaned)
|
|
411
412
|
|
|
412
|
-
|
|
413
|
+
if (Array.isArray(parsed)) return parsed as Candidate[]
|
|
413
414
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
415
|
+
// Some providers wrap in {"variants": [...]} or {"candidates": [...]}
|
|
416
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
417
|
+
for (const key of ["variants", "candidates", "results"]) {
|
|
418
|
+
const v = (parsed as Record<string, unknown>)[key]
|
|
418
419
|
|
|
419
|
-
|
|
420
|
-
}
|
|
420
|
+
if (Array.isArray(v)) return v as Candidate[]
|
|
421
421
|
}
|
|
422
|
-
} catch {
|
|
423
|
-
// fall through
|
|
424
422
|
}
|
|
425
423
|
|
|
426
424
|
return []
|
|
@@ -555,7 +553,14 @@ export async function expandGolden(
|
|
|
555
553
|
|
|
556
554
|
await Promise.all(workers)
|
|
557
555
|
|
|
558
|
-
|
|
556
|
+
{
|
|
557
|
+
await using out = createNewlineWriter(outputPath)
|
|
558
|
+
|
|
559
|
+
for (const row of outRows) {
|
|
560
|
+
await out.write(JSON.stringify(row))
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
559
564
|
report?.(`=== summary ===`)
|
|
560
565
|
report?.(`seeds processed: ${seeds.length}`)
|
|
561
566
|
report?.(`candidates kept: ${kept}`)
|