@mailwoman/corpus 7.2.0 → 7.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/package.json +59 -27
  2. package/src/adapter.ts +153 -0
  3. package/src/adapters/ban/adapter.ts +189 -0
  4. package/src/adapters/ban/street-decompose.ts +98 -0
  5. package/src/adapters/fcc-bdc/adapter.ts +188 -0
  6. package/src/adapters/geonames/adapter.ts +185 -0
  7. package/src/adapters/geonames-postal/adapter.ts +111 -0
  8. package/src/adapters/gnaf/adapter.ts +139 -0
  9. package/src/adapters/gnaf/assemble.ts +200 -0
  10. package/src/adapters/index.ts +135 -0
  11. package/src/adapters/openaddresses/adapter.ts +237 -0
  12. package/src/adapters/overture/adapter.ts +149 -0
  13. package/src/adapters/state-hi-schools/adapter.ts +171 -0
  14. package/src/adapters/state-ia-contractors/adapter.ts +148 -0
  15. package/src/adapters/state-ny-notaries/adapter.ts +169 -0
  16. package/src/adapters/state-tx-notaries/adapter.ts +160 -0
  17. package/src/adapters/synth-po-box/adapter.ts +197 -0
  18. package/src/adapters/tiger/adapter.ts +232 -0
  19. package/src/adapters/tiger/street-decompose.ts +120 -0
  20. package/src/adapters/usgov-hrsa-fqhc/adapter.ts +189 -0
  21. package/src/adapters/usgov-imls-pls/adapter.ts +152 -0
  22. package/src/adapters/usgov-irs-bmf/adapter.ts +155 -0
  23. package/src/adapters/usgov-nad/adapter.ts +338 -0
  24. package/src/adapters/usgov-nppes/adapter.ts +169 -0
  25. package/src/adapters/usgov-samhsa-treatment-locator/adapter.ts +211 -0
  26. package/src/adapters/wof-admin-jp/adapter.ts +176 -0
  27. package/src/adapters/wof-admin-json/adapter.ts +280 -0
  28. package/src/adapters/wof-postalcode-json/adapter.ts +209 -0
  29. package/src/align.ts +317 -0
  30. package/src/build.ts +348 -0
  31. package/src/codex/us-fips-state.ts +109 -0
  32. package/src/format.ts +14 -0
  33. package/src/golden.ts +147 -0
  34. package/src/index.ts +28 -0
  35. package/src/license.ts +48 -0
  36. package/src/parquet-wrapper/index.ts +9 -0
  37. package/src/parquet-wrapper/reader.ts +67 -0
  38. package/src/parquet-wrapper/schema.ts +80 -0
  39. package/src/parquet-wrapper/writer.ts +102 -0
  40. package/src/parquet.ts +360 -0
  41. package/src/runner.ts +283 -0
  42. package/src/shard-recipes/anchor-absorption.ts +86 -0
  43. package/src/shard-recipes/boundary-stress.ts +87 -0
  44. package/src/shard-recipes/country-balanced.ts +471 -0
  45. package/src/shard-recipes/cz-pcfirst-preposition.ts +83 -0
  46. package/src/shard-recipes/fr-admin-split.ts +212 -0
  47. package/src/shard-recipes/fr-bare-street.ts +85 -0
  48. package/src/shard-recipes/fr-fragment.ts +304 -0
  49. package/src/shard-recipes/fr-order.ts +283 -0
  50. package/src/shard-recipes/german.ts +206 -0
  51. package/src/shard-recipes/house-venue.ts +78 -0
  52. package/src/shard-recipes/index.ts +76 -0
  53. package/src/shard-recipes/intersection.ts +617 -0
  54. package/src/shard-recipes/locale.ts +415 -0
  55. package/src/shard-recipes/nl-postcode.ts +105 -0
  56. package/src/shard-recipes/no-fragment.ts +206 -0
  57. package/src/shard-recipes/no-street-led.ts +130 -0
  58. package/src/shard-recipes/no-street.ts +78 -0
  59. package/src/shard-recipes/po-box-cedex.ts +853 -0
  60. package/src/shard-recipes/po-box.ts +111 -0
  61. package/src/shard-recipes/scaffold.ts +187 -0
  62. package/src/shard-recipes/si-bare-village.ts +88 -0
  63. package/src/shard-recipes/street-affix.ts +549 -0
  64. package/src/shard-recipes/street-bare.ts +90 -0
  65. package/src/shard-recipes/street.ts +74 -0
  66. package/src/shard-recipes/unit.ts +315 -0
  67. package/src/split.ts +251 -0
  68. package/src/synthesize-anchor-absorption.ts +263 -0
  69. package/src/synthesize-boundary-stress.ts +504 -0
  70. package/src/synthesize-german.ts +227 -0
  71. package/src/synthesize-house-venue.ts +192 -0
  72. package/src/synthesize-intersection.ts +205 -0
  73. package/src/synthesize-no-street.ts +341 -0
  74. package/src/synthesize-po-box.ts +307 -0
  75. package/src/synthesize-street.ts +264 -0
  76. package/src/synthesize.ts +872 -0
  77. package/src/tokenize.ts +68 -0
  78. package/src/tools/align-shard.ts +67 -0
  79. package/src/tools/audit.ts +352 -0
  80. package/src/tools/corpus-stats.ts +183 -0
  81. package/src/tools/fetch/ban.ts +288 -0
  82. package/src/tools/fetch/download.ts +119 -0
  83. package/src/tools/fetch/hrsa.ts +65 -0
  84. package/src/tools/fetch/imls-pls.ts +162 -0
  85. package/src/tools/fetch/index.ts +121 -0
  86. package/src/tools/fetch/nad.ts +304 -0
  87. package/src/tools/fetch/nppes.ts +177 -0
  88. package/src/tools/fetch/openaddresses.ts +380 -0
  89. package/src/tools/fetch/state-hi-schools.ts +217 -0
  90. package/src/tools/fetch/state-sources.ts +142 -0
  91. package/src/tools/fetch/tiger-full.ts +318 -0
  92. package/src/tools/golden-expand.ts +524 -0
  93. package/src/tools/golden-promote.ts +304 -0
  94. package/src/tools/index.ts +24 -0
  95. package/src/tools/ingest-csv.ts +420 -0
  96. package/src/tools/jsonl-to-parquet.ts +200 -0
  97. package/src/tools/lint-shard-vocab.ts +357 -0
  98. package/src/tools/lint-shard.ts +502 -0
  99. package/src/tools/overlay-manifest.ts +141 -0
  100. package/src/tools/shard-kryptonite.ts +131 -0
  101. package/src/tools/shard-translit.ts +244 -0
  102. package/src/types.ts +213 -0
  103. package/src/wof-json.ts +236 -0
@@ -0,0 +1,338 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `usgov-nad`: US DOT National Address Database — ~97M structured address-point records.
7
+ *
8
+ * The single largest US address source available — federal aggregation of state + local 911-grade
9
+ * address points (every addressable location). Compared to TIGER ADDRFEAT (~20M segment-level, no
10
+ * city/locality) and NPPES (~7M provider-centric venues), NAD covers the entire residential +
11
+ * commercial address space with full structured components.
12
+ *
13
+ * The adapter consumes NDJSON shards produced by `fetch-nad.ts`'s featureserver mode (operator
14
+ * pre-downloads via `mailwoman corpus fetch nad`). Each shard is per-OID-range
15
+ * `oids_<start>-<end>.ndjson` with a sibling `.manifest.json`. Adapter iterates every `.ndjson`
16
+ * in the input directory, skipping the `quarantined-bash-bug/` subdir (legacy of the bash-
17
+ * fetcher's silent-page-failure bug).
18
+ *
19
+ * Field mapping (NAD v9 → CanonicalRow components):
20
+ *
21
+ * - House_number: `AddNo_Full` (pre-composed); falls back to AddNum_Pre + Add_Number + AddNum_Suf
22
+ * - Street: `StNam_Full` (pre-composed); falls back to St_PreDir + St_PreTyp + St_Name + St_PosTyp
23
+ *
24
+ * - St_PosDir + St_PosMod composition
25
+ * - Locality: `Post_City` > `Inc_Muni` > `Census_Plc` > `Uninc_Comm` (first non-empty)
26
+ * - Region: `State` (2-char USPS code, including territories: PR, GU, VI, AS, MP)
27
+ * - Postcode: `Zip_Code` + `Plus_4` (joined as `XXXXX-NNNN` when both present)
28
+ * - Venue: `LandmkName` (typically a park, school, hospital, named facility — when present)
29
+ *
30
+ * License: stamped `"Public Domain"` per 17 U.S.C. § 105 (US federal works).
31
+ */
32
+
33
+ import { readdir } from "node:fs/promises"
34
+ import { join } from "node:path"
35
+
36
+ import { TextSpliterator } from "spliterator"
37
+
38
+ import { reconcileComponents } from "../../format.ts"
39
+ import type { AdapterOptions, CanonicalRow, CorpusAdapter } from "../../types.ts"
40
+
41
+ export const USGOV_NAD_ADAPTER_ID = "usgov-nad"
42
+ export const USGOV_NAD_DEFAULT_LICENSE = "Public Domain"
43
+
44
+ interface NADRecord {
45
+ OBJECTID?: number
46
+ UUID?: string | null
47
+ // House number
48
+ AddNum_Pre?: string | null
49
+ Add_Number?: number | string | null
50
+ AddNum_Suf?: string | null
51
+ AddNo_Full?: string | null
52
+ // Street parts
53
+ St_PreMod?: string | null
54
+ St_PreDir?: string | null
55
+ St_PreTyp?: string | null
56
+ St_PreSep?: string | null
57
+ St_Name?: string | null
58
+ St_PosTyp?: string | null
59
+ St_PosDir?: string | null
60
+ St_PosMod?: string | null
61
+ StNam_Full?: string | null
62
+ // Sub-address (carried as part of street for now; Phase 1 has no unit/floor labels)
63
+ Building?: string | null
64
+ Floor?: string | null
65
+ Unit?: string | null
66
+ Room?: string | null
67
+ Seat?: string | null
68
+ Addtl_Loc?: string | null
69
+ SubAddress?: string | null
70
+ // Landmark / venue
71
+ LandmkName?: string | null
72
+ // Locality alternates (we prefer Post_City for what a human would type)
73
+ County?: string | null
74
+ Inc_Muni?: string | null
75
+ Post_City?: string | null
76
+ Census_Plc?: string | null
77
+ Uninc_Comm?: string | null
78
+ Nbrhd_Comm?: string | null
79
+ NatAmArea?: string | null
80
+ NatAmSub?: string | null
81
+ Urbnztn_PR?: string | null
82
+ PlaceOther?: string | null
83
+ PlaceNmTyp?: string | null
84
+ // State + ZIP
85
+ State?: string | null
86
+ Zip_Code?: string | null
87
+ Plus_4?: string | null
88
+ }
89
+
90
+ const US_STATES_SET = new Set([
91
+ "AL",
92
+ "AK",
93
+ "AZ",
94
+ "AR",
95
+ "CA",
96
+ "CO",
97
+ "CT",
98
+ "DE",
99
+ "DC",
100
+ "FL",
101
+ "GA",
102
+ "HI",
103
+ "ID",
104
+ "IL",
105
+ "IN",
106
+ "IA",
107
+ "KS",
108
+ "KY",
109
+ "LA",
110
+ "ME",
111
+ "MD",
112
+ "MA",
113
+ "MI",
114
+ "MN",
115
+ "MS",
116
+ "MO",
117
+ "MT",
118
+ "NE",
119
+ "NV",
120
+ "NH",
121
+ "NJ",
122
+ "NM",
123
+ "NY",
124
+ "NC",
125
+ "ND",
126
+ "OH",
127
+ "OK",
128
+ "OR",
129
+ "PA",
130
+ "RI",
131
+ "SC",
132
+ "SD",
133
+ "TN",
134
+ "TX",
135
+ "UT",
136
+ "VT",
137
+ "VA",
138
+ "WA",
139
+ "WV",
140
+ "WI",
141
+ "WY",
142
+ // Territories that ship in NAD
143
+ "PR",
144
+ "GU",
145
+ "VI",
146
+ "AS",
147
+ "MP",
148
+ ])
149
+
150
+ function nonEmpty(...values: Array<string | null | undefined>): string | undefined {
151
+ for (const v of values) {
152
+ const trimmed = (v ?? "").toString().trim()
153
+
154
+ if (trimmed) return trimmed
155
+ }
156
+
157
+ return undefined
158
+ }
159
+
160
+ function composeHouseNumber(r: NADRecord): string | undefined {
161
+ const full = (r.AddNo_Full ?? "").toString().trim()
162
+
163
+ if (full) return full
164
+ const num = r.Add_Number == null ? "" : String(r.Add_Number).trim()
165
+
166
+ if (!num) return undefined
167
+ const pre = (r.AddNum_Pre ?? "").toString().trim()
168
+ const suf = (r.AddNum_Suf ?? "").toString().trim()
169
+
170
+ return [pre, num, suf].filter(Boolean).join(" ").trim() || undefined
171
+ }
172
+
173
+ interface DecomposedNADStreet {
174
+ prefix?: string
175
+ street?: string
176
+ suffix?: string
177
+ full: string
178
+ }
179
+
180
+ function decomposeNADStreet(r: NADRecord): DecomposedNADStreet | undefined {
181
+ const name = (r.St_Name ?? "").toString().trim()
182
+
183
+ if (name) {
184
+ const preDir = (r.St_PreDir ?? "").toString().trim()
185
+ const preTyp = (r.St_PreTyp ?? "").toString().trim()
186
+ const preSep = (r.St_PreSep ?? "").toString().trim()
187
+ const posTyp = (r.St_PosTyp ?? "").toString().trim()
188
+ const posDir = (r.St_PosDir ?? "").toString().trim()
189
+ const prefix = [preDir, preTyp, preSep].filter(Boolean).join(" ") || undefined
190
+ const suffix = [posTyp, posDir].filter(Boolean).join(" ") || undefined
191
+ const full = [prefix, name, suffix].filter(Boolean).join(" ")
192
+
193
+ return { prefix, street: name, suffix, full }
194
+ }
195
+ const full = (r.StNam_Full ?? "").toString().trim()
196
+
197
+ if (full) return { full, street: full }
198
+
199
+ return undefined
200
+ }
201
+
202
+ function composeLocality(r: NADRecord): string | undefined {
203
+ return nonEmpty(r.Post_City, r.Inc_Muni, r.Census_Plc, r.Uninc_Comm)
204
+ }
205
+
206
+ function composePostcode(r: NADRecord): string | undefined {
207
+ const zip = (r.Zip_Code ?? "").toString().trim()
208
+
209
+ if (!zip) return undefined
210
+ const plus4 = (r.Plus_4 ?? "").toString().trim()
211
+
212
+ return plus4 ? `${zip}-${plus4}` : zip
213
+ }
214
+
215
+ function composeRaw(parts: {
216
+ venue?: string
217
+ houseNumber?: string
218
+ street?: string
219
+ unit?: string
220
+ locality: string
221
+ region: string
222
+ postcode: string
223
+ }): string {
224
+ const streetLine = [parts.houseNumber, parts.street, parts.unit].filter(Boolean).join(" ").trim()
225
+ const tail = `${parts.locality}, ${parts.region} ${parts.postcode}`
226
+
227
+ return [parts.venue, streetLine || undefined, tail].filter(Boolean).join(", ")
228
+ }
229
+
230
+ export function createUsgovNADAdapter(): CorpusAdapter {
231
+ return {
232
+ id: USGOV_NAD_ADAPTER_ID,
233
+ defaultLicense: USGOV_NAD_DEFAULT_LICENSE,
234
+ description:
235
+ "US DOT National Address Database — ~97M structured US address points (911-grade). Single largest US source.",
236
+
237
+ async *rows(opts: AdapterOptions): AsyncIterable<CanonicalRow> {
238
+ if (opts.country && opts.country !== "US") {
239
+ throw new Error(`usgov-nad adapter: only US supported, got country=${opts.country}`)
240
+ }
241
+
242
+ // inputPath is a directory of NDJSON shards (per fetch-nad.ts featureserver output).
243
+ // Single-file inputs (e.g. a bulk-extracted CSV) are not currently supported — the
244
+ // featureserver shard pattern is the primary distribution.
245
+ const entries = await readdir(opts.inputPath)
246
+ const shards = entries.filter((n) => n.endsWith(".ndjson")).sort()
247
+
248
+ let emitted = 0
249
+ outer: for (const shard of shards) {
250
+ if (opts.signal?.aborted) break
251
+ // TextSpliterator streams string lines; the per-line try/catch below keeps the reader
252
+ // tolerant of malformed rows (skip silently), so TextSpliterator + explicit JSON.parse —
253
+ // not JSONSpliterator, which would throw. The path string lets the lib own + dispose each
254
+ // shard's file handle, including on the `break outer` early exit.
255
+ const lines = TextSpliterator.fromAsync(join(opts.inputPath, shard))
256
+
257
+ for await (const line of lines) {
258
+ if (opts.signal?.aborted) break outer
259
+
260
+ if (opts.limit !== undefined && emitted >= opts.limit) break outer
261
+
262
+ if (!line) continue
263
+
264
+ let record: NADRecord
265
+
266
+ try {
267
+ record = JSON.parse(line) as NADRecord
268
+ } catch {
269
+ continue // malformed line — skip silently
270
+ }
271
+
272
+ const state = (record.State ?? "").toString().trim().toUpperCase()
273
+
274
+ if (!US_STATES_SET.has(state)) continue
275
+
276
+ const locality = composeLocality(record)
277
+
278
+ if (!locality) continue
279
+
280
+ const postcode = composePostcode(record)
281
+
282
+ if (!postcode) continue
283
+
284
+ const decomposed = decomposeNADStreet(record)
285
+ const houseNumber = composeHouseNumber(record)
286
+ const venue = nonEmpty(record.LandmkName)
287
+ const unit = nonEmpty(record.Unit, record.Building, record.Floor, record.Room)
288
+
289
+ const components: CanonicalRow["components"] = {
290
+ ...(venue ? { venue } : {}),
291
+ ...(houseNumber ? { house_number: houseNumber } : {}),
292
+ ...(decomposed?.prefix ? { street_prefix: decomposed.prefix } : {}),
293
+ ...(decomposed?.street ? { street: decomposed.street } : {}),
294
+ ...(decomposed?.suffix ? { street_suffix: decomposed.suffix } : {}),
295
+ ...(unit ? { unit } : {}),
296
+ locality,
297
+ region: state,
298
+ postcode,
299
+ }
300
+
301
+ const raw = composeRaw({
302
+ venue,
303
+ houseNumber,
304
+ street: decomposed?.full,
305
+ unit,
306
+ locality,
307
+ region: state,
308
+ postcode,
309
+ })
310
+
311
+ if (!raw) continue
312
+
313
+ const aligned = reconcileComponents(components, raw)
314
+
315
+ if (Object.keys(aligned).length <= 2) continue
316
+
317
+ const sourceID = record.UUID
318
+ ? `${USGOV_NAD_ADAPTER_ID}-${record.UUID}`
319
+ : `${USGOV_NAD_ADAPTER_ID}-${record.OBJECTID ?? `${shard}:${emitted}`}`
320
+
321
+ yield {
322
+ raw,
323
+ components: aligned,
324
+ country: "US",
325
+ locale: "en-US",
326
+ source: USGOV_NAD_ADAPTER_ID,
327
+ source_id: sourceID,
328
+ corpus_version: "",
329
+ license: USGOV_NAD_DEFAULT_LICENSE,
330
+ }
331
+ emitted++
332
+ }
333
+ }
334
+ },
335
+ }
336
+ }
337
+
338
+ export const usgovNADAdapter = createUsgovNADAdapter()
@@ -0,0 +1,169 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `usgov-nppes`: CMS National Plan and Provider Enumeration System (NPI registry) CSV consumer.
7
+ *
8
+ * NPPES is the authoritative US healthcare provider registry, published monthly by CMS. Each row
9
+ * carries a provider's business practice location address together with their legal business name
10
+ * or individual name. At ~7M rows it is the single largest venue+address signal source
11
+ * available.
12
+ *
13
+ * The adapter consumes the monthly full-replacement CSV (operator pre-downloads via
14
+ * `fetch-nppes.ts`). Column names match the canonical NPPES "Full Replacement Monthly NPI File"
15
+ * header published at `https://download.cms.gov/nppes/NPI_Files.html`.
16
+ *
17
+ * Output: one row per CSV record where the practice location address is populated. Organization
18
+ * rows carry `venue` from the legal business name; individual rows compose `attention` from
19
+ * last+first name. Address quad goes on `(house_number, street, locality, region, postcode)`.
20
+ *
21
+ * License: stamped `"Public Domain"` per CMS's federal government distribution terms.
22
+ */
23
+
24
+ import { createReadStream } from "node:fs"
25
+
26
+ import { parse as csvParse } from "csv-parse"
27
+
28
+ import { stableSourceID } from "../../adapter.ts"
29
+ import { lookupStateAbbreviation } from "../../codex/us-fips-state.ts"
30
+ import { reconcileComponents } from "../../format.ts"
31
+ import type { AdapterOptions, CanonicalRow, CorpusAdapter } from "../../types.ts"
32
+
33
+ export const USGOV_NPPES_ADAPTER_ID = "usgov-nppes"
34
+ export const USGOV_NPPES_DEFAULT_LICENSE = "Public Domain"
35
+
36
+ const HOUSE_NUMBER_PREFIX = /^(\d+(?:-\d+)?[A-Za-z]?)\s+(.+)$/
37
+
38
+ interface NPPESRow {
39
+ NPI: string
40
+ "Entity Type Code": string
41
+ "Provider Organization Name (Legal Business Name)": string
42
+ "Provider Last Name (Legal Name)": string
43
+ "Provider First Name": string
44
+ "Provider First Line Business Practice Location Address": string
45
+ "Provider Second Line Business Practice Location Address": string
46
+ "Provider Business Practice Location Address City Name": string
47
+ "Provider Business Practice Location Address State Name": string
48
+ "Provider Business Practice Location Address Postal Code": string
49
+ }
50
+
51
+ function splitAddress(address: string): { house_number?: string; street: string } | null {
52
+ const trimmed = address.trim()
53
+
54
+ if (!trimmed) return null
55
+ const m = HOUSE_NUMBER_PREFIX.exec(trimmed)
56
+
57
+ if (m) return { house_number: m[1], street: m[2]!.trim() }
58
+
59
+ return { street: trimmed }
60
+ }
61
+
62
+ function composeRaw(
63
+ venue: string | undefined,
64
+ house: string | undefined,
65
+ street: string,
66
+ city: string,
67
+ state: string,
68
+ postcode: string
69
+ ): string {
70
+ const streetPart = [house, street].filter(Boolean).join(" ").trim()
71
+ const cityPart = [city.trim(), [state, postcode].filter(Boolean).join(" ").trim()].filter(Boolean).join(", ")
72
+
73
+ return [venue, streetPart, cityPart].filter(Boolean).join(", ")
74
+ }
75
+
76
+ export function createUsgovNPPESAdapter(): CorpusAdapter {
77
+ return {
78
+ id: USGOV_NPPES_ADAPTER_ID,
79
+ defaultLicense: USGOV_NPPES_DEFAULT_LICENSE,
80
+ description:
81
+ "CMS National Plan and Provider Enumeration System — 7M provider practice locations (public-domain). Venue+address co-occurrence at scale.",
82
+
83
+ async *rows(opts: AdapterOptions): AsyncIterable<CanonicalRow> {
84
+ if (opts.country && opts.country !== "US") {
85
+ throw new Error(`usgov-nppes adapter: only US supported, got country=${opts.country}`)
86
+ }
87
+
88
+ const stream = createReadStream(opts.inputPath, { encoding: "utf8" })
89
+ const parser = stream.pipe(
90
+ csvParse({
91
+ columns: true,
92
+ skip_empty_lines: true,
93
+ relax_quotes: true,
94
+ relax_column_count: true,
95
+ })
96
+ )
97
+
98
+ let emitted = 0
99
+
100
+ try {
101
+ for await (const record of parser as AsyncIterable<NPPESRow>) {
102
+ if (opts.signal?.aborted) break
103
+
104
+ if (opts.limit !== undefined && emitted >= opts.limit) break
105
+
106
+ const npi = (record.NPI ?? "").trim()
107
+ const entityType = (record["Entity Type Code"] ?? "").trim()
108
+ const orgName = (record["Provider Organization Name (Legal Business Name)"] ?? "").trim()
109
+ const lastName = (record["Provider Last Name (Legal Name)"] ?? "").trim()
110
+ const firstName = (record["Provider First Name"] ?? "").trim()
111
+
112
+ const address1 = (record["Provider First Line Business Practice Location Address"] ?? "").trim()
113
+ const address2 = (record["Provider Second Line Business Practice Location Address"] ?? "").trim()
114
+ const city = (record["Provider Business Practice Location Address City Name"] ?? "").trim()
115
+ const stateRaw = (record["Provider Business Practice Location Address State Name"] ?? "").trim()
116
+ const postcode = (record["Provider Business Practice Location Address Postal Code"] ?? "").trim()
117
+
118
+ if (!city || !postcode) continue
119
+
120
+ const state = lookupStateAbbreviation(stateRaw)
121
+
122
+ if (!state) continue
123
+
124
+ const fullStreet = [address1, address2].filter(Boolean).join(" ")
125
+ const split = splitAddress(fullStreet)
126
+
127
+ if (!split) continue
128
+
129
+ const venue = orgName || [firstName, lastName].filter(Boolean).join(" ") || undefined
130
+
131
+ const components: CanonicalRow["components"] = {
132
+ ...(venue ? { venue } : {}),
133
+ ...(split.house_number ? { house_number: split.house_number } : {}),
134
+ street: split.street,
135
+ locality: city,
136
+ region: state.abbreviation,
137
+ postcode,
138
+ }
139
+
140
+ const raw = composeRaw(venue, split.house_number, split.street, city, state.abbreviation, postcode)
141
+
142
+ if (!raw) continue
143
+
144
+ const aligned = reconcileComponents(components, raw)
145
+
146
+ if (Object.keys(aligned).length <= 2) continue
147
+
148
+ const sourceID = npi ? `${USGOV_NPPES_ADAPTER_ID}-${npi}` : stableSourceID(USGOV_NPPES_ADAPTER_ID, aligned)
149
+
150
+ yield {
151
+ raw,
152
+ components: aligned,
153
+ country: "US",
154
+ locale: "en-US",
155
+ source: USGOV_NPPES_ADAPTER_ID,
156
+ source_id: sourceID,
157
+ corpus_version: "",
158
+ license: USGOV_NPPES_DEFAULT_LICENSE,
159
+ }
160
+ emitted++
161
+ }
162
+ } finally {
163
+ stream.destroy()
164
+ }
165
+ },
166
+ }
167
+ }
168
+
169
+ export const usgovNPPESAdapter = createUsgovNPPESAdapter()