@mailwoman/bdc 8.3.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.
Files changed (87) hide show
  1. package/README.md +7 -0
  2. package/index.ts +2 -0
  3. package/out/index.d.ts +3 -0
  4. package/out/index.d.ts.map +1 -0
  5. package/out/index.js +3 -0
  6. package/out/index.js.map +1 -0
  7. package/out/schema.d.ts +138 -0
  8. package/out/schema.d.ts.map +1 -0
  9. package/out/schema.js +88 -0
  10. package/out/schema.js.map +1 -0
  11. package/out/sdk/build-bdc.d.ts +214 -0
  12. package/out/sdk/build-bdc.d.ts.map +1 -0
  13. package/out/sdk/build-bdc.js +500 -0
  14. package/out/sdk/build-bdc.js.map +1 -0
  15. package/out/sdk/buildings.d.ts +84 -0
  16. package/out/sdk/buildings.d.ts.map +1 -0
  17. package/out/sdk/buildings.js +75 -0
  18. package/out/sdk/buildings.js.map +1 -0
  19. package/out/sdk/client.d.ts +237 -0
  20. package/out/sdk/client.d.ts.map +1 -0
  21. package/out/sdk/client.js +430 -0
  22. package/out/sdk/client.js.map +1 -0
  23. package/out/sdk/common.d.ts +279 -0
  24. package/out/sdk/common.d.ts.map +1 -0
  25. package/out/sdk/common.js +162 -0
  26. package/out/sdk/common.js.map +1 -0
  27. package/out/sdk/data-collection.d.ts +244 -0
  28. package/out/sdk/data-collection.d.ts.map +1 -0
  29. package/out/sdk/data-collection.js +50 -0
  30. package/out/sdk/data-collection.js.map +1 -0
  31. package/out/sdk/download.d.ts +36 -0
  32. package/out/sdk/download.d.ts.map +1 -0
  33. package/out/sdk/download.js +76 -0
  34. package/out/sdk/download.js.map +1 -0
  35. package/out/sdk/filing-dates.d.ts +57 -0
  36. package/out/sdk/filing-dates.d.ts.map +1 -0
  37. package/out/sdk/filing-dates.js +50 -0
  38. package/out/sdk/filing-dates.js.map +1 -0
  39. package/out/sdk/filing-landscape.d.ts +124 -0
  40. package/out/sdk/filing-landscape.d.ts.map +1 -0
  41. package/out/sdk/filing-landscape.js +216 -0
  42. package/out/sdk/filing-landscape.js.map +1 -0
  43. package/out/sdk/index.d.ts +15 -0
  44. package/out/sdk/index.d.ts.map +1 -0
  45. package/out/sdk/index.js +15 -0
  46. package/out/sdk/index.js.map +1 -0
  47. package/out/sdk/list-files.d.ts +39 -0
  48. package/out/sdk/list-files.d.ts.map +1 -0
  49. package/out/sdk/list-files.js +26 -0
  50. package/out/sdk/list-files.js.map +1 -0
  51. package/out/sdk/location.d.ts +40 -0
  52. package/out/sdk/location.d.ts.map +1 -0
  53. package/out/sdk/location.js +23 -0
  54. package/out/sdk/location.js.map +1 -0
  55. package/out/sdk/nearest-infrastructure.d.ts +84 -0
  56. package/out/sdk/nearest-infrastructure.d.ts.map +1 -0
  57. package/out/sdk/nearest-infrastructure.js +84 -0
  58. package/out/sdk/nearest-infrastructure.js.map +1 -0
  59. package/out/sdk/parsing.d.ts +57 -0
  60. package/out/sdk/parsing.d.ts.map +1 -0
  61. package/out/sdk/parsing.js +87 -0
  62. package/out/sdk/parsing.js.map +1 -0
  63. package/out/sdk/plausibility.d.ts +256 -0
  64. package/out/sdk/plausibility.d.ts.map +1 -0
  65. package/out/sdk/plausibility.js +334 -0
  66. package/out/sdk/plausibility.js.map +1 -0
  67. package/out/sdk/technologies.d.ts +229 -0
  68. package/out/sdk/technologies.d.ts.map +1 -0
  69. package/out/sdk/technologies.js +217 -0
  70. package/out/sdk/technologies.js.map +1 -0
  71. package/package.json +97 -0
  72. package/schema.ts +172 -0
  73. package/sdk/build-bdc.ts +810 -0
  74. package/sdk/buildings.ts +87 -0
  75. package/sdk/client.ts +643 -0
  76. package/sdk/common.ts +363 -0
  77. package/sdk/data-collection.ts +268 -0
  78. package/sdk/download.ts +91 -0
  79. package/sdk/filing-dates.ts +88 -0
  80. package/sdk/filing-landscape.ts +287 -0
  81. package/sdk/index.ts +14 -0
  82. package/sdk/list-files.ts +72 -0
  83. package/sdk/location.ts +45 -0
  84. package/sdk/nearest-infrastructure.ts +129 -0
  85. package/sdk/parsing.ts +124 -0
  86. package/sdk/plausibility.ts +512 -0
  87. package/sdk/technologies.ts +258 -0
package/sdk/common.ts ADDED
@@ -0,0 +1,363 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ * @file BDC file metadata model — dictionaries for the FCC's `bdc_file` listing rows, the
6
+ * raw-to-parsed record shape, and file-ordering comparators.
7
+ */
8
+
9
+ import type { Tagged } from "type-fest"
10
+
11
+ import type { BroadbandTechnologyCode } from "./technologies.ts"
12
+
13
+ /**
14
+ * Unique identifier for an FCC BDC broadband provider.
15
+ *
16
+ * Re-homed locally as `Tagged<number, ...>` — the Nexus original imported this from `@isp.nexus/fcc`
17
+ * (`BroadbandProvider.ts`), where it's defined identically.
18
+ *
19
+ * @category BDC
20
+ * @category FCC
21
+ */
22
+ export type ProviderID = Tagged<number, "ProviderID">
23
+
24
+ /**
25
+ * The data type of the file, e.g. what kind of data is in the file.
26
+ *
27
+ * @category BDC
28
+ * @category FCC
29
+ */
30
+ export const BDCFilingDataType = {
31
+ /**
32
+ * The file contains data about the availability of broadband with a specific provider.
33
+ */
34
+ Availability: "availability",
35
+ /**
36
+ * The file contains data about the challenge process.
37
+ */
38
+ Challenge: "challenge",
39
+ } as const
40
+
41
+ /**
42
+ * @category BDC
43
+ * @category FCC
44
+ */
45
+ export type BDCFilingDataType = (typeof BDCFilingDataType)[keyof typeof BDCFilingDataType]
46
+
47
+ /**
48
+ * @category BDC
49
+ * @category FCC
50
+ */
51
+ export const BDCGISFileType = {
52
+ ShapeFile: 1,
53
+ GeoPackage: 2,
54
+ } as const
55
+
56
+ /**
57
+ * @category BDC
58
+ * @category FCC
59
+ */
60
+ export type BDCGISFileType = (typeof BDCGISFileType)[keyof typeof BDCGISFileType]
61
+
62
+ /**
63
+ * The type of file, e.g. what format the file is in.
64
+ *
65
+ * @category BDC
66
+ * @category FCC
67
+ */
68
+ export const BDCFileFormat = {
69
+ CSV: "csv",
70
+ GIS: "gis",
71
+ } as const
72
+
73
+ /**
74
+ * @category BDC
75
+ * @category FCC
76
+ */
77
+ export type BDCFileFormat = (typeof BDCFileFormat)[keyof typeof BDCFileFormat]
78
+
79
+ /**
80
+ * @category BDC
81
+ * @category FCC
82
+ */
83
+ export const BDCFileCategory = {
84
+ Provider: "Provider",
85
+ Summary: "Summary",
86
+ State: "State",
87
+ } as const
88
+
89
+ /**
90
+ * @category BDC
91
+ * @category FCC
92
+ */
93
+ export type BDCFileCategory = (typeof BDCFileCategory)[keyof typeof BDCFileCategory]
94
+
95
+ /**
96
+ * @category BDC
97
+ * @category FCC
98
+ */
99
+ export const BDCProviderSubCategory = {
100
+ FixedBroadband: "Fixed Broadband",
101
+ MobileBroadband: "Mobile Broadband",
102
+ MobileVoice: "Mobile Voice",
103
+ SupportingData: "Supporting Data",
104
+ } as const
105
+
106
+ /**
107
+ * @category BDC
108
+ * @category FCC
109
+ */
110
+ export type BDCProviderSubCategory = (typeof BDCProviderSubCategory)[keyof typeof BDCProviderSubCategory]
111
+
112
+ /**
113
+ * @category BDC
114
+ * @category FCC
115
+ */
116
+ export const BDCSummarySubCategory = {
117
+ BroadbandSummaryByGeography: "Broadband Summary by Geography Type",
118
+ ProviderSummaryByGeography: "Provider Summary by Geography Type",
119
+ ProviderSummaryFixedBroadband: "Provider Summary - Fixed Broadband",
120
+ ProviderSummaryMobileBroadband: "Provider Summary - Mobile Broadband",
121
+ } as const
122
+
123
+ /**
124
+ * @category BDC
125
+ * @category FCC
126
+ */
127
+ export type BDCSummarySubCategory = (typeof BDCSummarySubCategory)[keyof typeof BDCSummarySubCategory]
128
+
129
+ /**
130
+ * @category BDC
131
+ * @category FCC
132
+ */
133
+ export const BDCStateSubCategory = {
134
+ FixedBroadband: "Fixed Broadband",
135
+ MobileBroadband: "Mobile Broadband",
136
+ MobileVoice: "Mobile Voice",
137
+ } as const
138
+
139
+ /**
140
+ * @category BDC
141
+ * @category FCC
142
+ */
143
+ export type BDCStateSubCategory = (typeof BDCStateSubCategory)[keyof typeof BDCStateSubCategory]
144
+
145
+ export type BDCSubCategory = BDCProviderSubCategory | BDCStateSubCategory | BDCSummarySubCategory
146
+
147
+ /**
148
+ * A single row from the FCC's BDC file listing, as returned by the API before parsing.
149
+ *
150
+ * @category BDC
151
+ * @category FCC
152
+ */
153
+ export interface RawBDCFile {
154
+ file_id: number
155
+ category: BDCFileCategory
156
+ subcategory: BDCSubCategory
157
+ /**
158
+ * Comma-separated list of technology codes.
159
+ *
160
+ * Nullable in live data: the FCC's `/map/downloads/listAvailabilityData` response carries `technology_code: null` for
161
+ * at least some State-category rows (first observed in the live FCC smoke test, see
162
+ * `.superpowers/sdd/2026-07-30-bdc-2b-plan/live-smoke-findings.md`). Guarded in {@linkcode parseRawBDCFile} — a null
163
+ * value parses to an empty `technologyCodes` set rather than throwing.
164
+ *
165
+ * @see {@link BroadbandTechnologyCode}
166
+ */
167
+ technology_code: string | null
168
+ technology_code_desc: string
169
+ /**
170
+ * 2-digit state or territory FIPS code.
171
+ *
172
+ * Loosely typed as `string` for now. The Nexus original was `AdminLevel1Code` (via `@isp.nexus/tiger`); this port
173
+ * drops that dependency, same as `data-collection.ts`'s `FCCStateID`. Task 2c tightens this against
174
+ * `@mailwoman/tiger` if a downstream dictionary needs the literal union.
175
+ *
176
+ * Nullable in live data for rows not scoped to a specific state (e.g. Provider-category rows). Guarded in
177
+ * {@linkcode parseRawBDCFile} — a null value parses to an empty `stateCode` string.
178
+ */
179
+ state_fips: string | null
180
+ /**
181
+ * State or territory name.
182
+ *
183
+ * Loosely typed as `string` — the Nexus original was `StateName` (via `@isp.nexus/tiger`). Same task-2c deferral as
184
+ * `state_fips` above.
185
+ */
186
+ state_name: string
187
+ /**
188
+ * Nullable in live data for rows not scoped to a specific provider (e.g. State/Summary-category rows). Guarded in
189
+ * {@linkcode parseRawBDCFile} — a null value parses to a `providerID` of `0`.
190
+ */
191
+ provider_id: string | null
192
+ /**
193
+ * Nullable in live data — travels with `provider_id` (see above). Guarded in {@linkcode parseRawBDCFile} — a null
194
+ * value parses to an empty `providerName` string.
195
+ */
196
+ provider_name: string | null
197
+ file_type: string
198
+ file_name: string
199
+ record_count: string
200
+ }
201
+
202
+ /**
203
+ * A parsed FCC BDC file-listing entry.
204
+ *
205
+ * @category BDC
206
+ * @category FCC
207
+ */
208
+ export interface BDCFile {
209
+ /**
210
+ * Unique identifier for the file, defined by the FCC.
211
+ */
212
+ fileID: number
213
+
214
+ revision: Date
215
+ vintage: Date
216
+
217
+ /**
218
+ * The date the file was was downloaded, parsed, and stored in the database.
219
+ */
220
+ synchronizedAt?: Date
221
+
222
+ /**
223
+ * The category of the file.
224
+ */
225
+ category: BDCFileCategory
226
+ /**
227
+ * The subcategory of the file.
228
+ */
229
+ subcategory: BDCSubCategory
230
+ /**
231
+ * The technology codes in the file. Empty when the raw `technology_code` was `null`.
232
+ */
233
+ technologyCodes: Set<BroadbandTechnologyCode>
234
+ /**
235
+ * The state or territory FIPS code.
236
+ *
237
+ * Loosely typed as `string` — see {@linkcode RawBDCFile} for the task-2c deferral. Empty string when the raw
238
+ * `state_fips` was `null`.
239
+ */
240
+ stateCode: string
241
+ /**
242
+ * The provider ID associated with the file. `0` when the raw `provider_id` was `null` (no specific provider — see
243
+ * {@linkcode RawBDCFile}).
244
+ */
245
+ providerID: ProviderID
246
+ /**
247
+ * The provider name associated with the file. Empty string when the raw `provider_name` was `null`.
248
+ */
249
+ providerName: string
250
+ /**
251
+ * The number of records in the file.
252
+ */
253
+ recordCount: number
254
+ /**
255
+ * The type of file, e.g. what format the file is in.
256
+ */
257
+ fileType: string
258
+ /**
259
+ * The name of the file, as provided by the FCC.
260
+ */
261
+ fileName: string
262
+ }
263
+
264
+ const BDCFileNamePattern = /([A-Z])(\d+)_(\d{2})([a-z]{3})(\d{4})$/
265
+
266
+ const MonthAbbreviation = {
267
+ jan: 0,
268
+ feb: 1,
269
+ mar: 2,
270
+ apr: 3,
271
+ may: 4,
272
+ jun: 5,
273
+ jul: 6,
274
+ aug: 7,
275
+ sep: 8,
276
+ oct: 9,
277
+ nov: 10,
278
+ dec: 11,
279
+ } as const
280
+
281
+ export type MonthAbbreviation = keyof typeof MonthAbbreviation
282
+
283
+ const VintageMonthLetter = {
284
+ /**
285
+ * December
286
+ */
287
+ D: 11,
288
+ /**
289
+ * June
290
+ */
291
+ J: 5,
292
+ }
293
+
294
+ type VintageMonthLetter = keyof typeof VintageMonthLetter
295
+
296
+ /**
297
+ * Given a BDC file, parse the components of the file name.
298
+ */
299
+ export function parseBDCFileTimestamps(fileName: string) {
300
+ const match = fileName.match(BDCFileNamePattern)
301
+
302
+ if (!match) throw new Error(`Invalid BDC file name: ${fileName}`)
303
+ const [, vintageMonthLetter, vintageYearAbbreviation, revisionDay, revisionMonthAbbreviation, revisionYear] = match
304
+
305
+ const revisionMonth = MonthAbbreviation[revisionMonthAbbreviation as MonthAbbreviation]
306
+ const revision = new Date(Number.parseInt(revisionYear!, 10), revisionMonth, Number.parseInt(revisionDay!, 10))
307
+
308
+ const vintageMonth = VintageMonthLetter[vintageMonthLetter as VintageMonthLetter]
309
+ const vintageYear = Number.parseInt(`20${vintageYearAbbreviation}`, 10)
310
+
311
+ const vintage = new Date(vintageYear, vintageMonth)
312
+
313
+ return {
314
+ revision,
315
+ vintage,
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Parses a raw BDC file-listing entry into a {@linkcode BDCFile}.
321
+ */
322
+ export function parseRawBDCFile(raw: RawBDCFile): BDCFile {
323
+ const parsedBDC: BDCFile = {
324
+ ...parseBDCFileTimestamps(raw.file_name),
325
+ fileName: raw.file_name,
326
+ fileType: raw.file_type,
327
+ fileID: raw.file_id,
328
+ recordCount: Number.parseInt(raw.record_count, 10),
329
+ category: raw.category,
330
+ subcategory: raw.subcategory,
331
+ technologyCodes: new Set(
332
+ raw.technology_code === null
333
+ ? []
334
+ : raw.technology_code.split(",").map((code) => Number.parseInt(code, 10) as BroadbandTechnologyCode)
335
+ ),
336
+ stateCode: raw.state_fips ?? "",
337
+ providerID: (raw.provider_id === null ? 0 : Number.parseInt(raw.provider_id, 10)) as ProviderID,
338
+ providerName: raw.provider_name ?? "",
339
+ }
340
+
341
+ return parsedBDC
342
+ }
343
+
344
+ /**
345
+ * Comparator for sorting {@linkcode BDCFile} records ascending by revision date.
346
+ */
347
+ export function compareRevisionAsc(a: BDCFile, b: BDCFile): number {
348
+ return a.revision.getTime() - b.revision.getTime()
349
+ }
350
+
351
+ /**
352
+ * Comparator for sorting {@linkcode BDCFile} records ascending by provider ID.
353
+ */
354
+ export function compareProviderIDAsc(a: BDCFile, b: BDCFile): number {
355
+ return a.providerID - b.providerID
356
+ }
357
+
358
+ /**
359
+ * Comparator for sorting {@linkcode BDCFile} records ascending by state FIPS code.
360
+ */
361
+ export function compareStateCodeAsc(a: BDCFile, b: BDCFile): number {
362
+ return Number.parseInt(a.stateCode, 10) - Number.parseInt(b.stateCode, 10)
363
+ }
@@ -0,0 +1,268 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ * @file FCC Broadband Data Collection System
6
+ */
7
+
8
+ import type { BuildingTypeCode } from "./buildings.ts"
9
+ import type { BroadbandServicableLocationID } from "./location.ts"
10
+
11
+ /**
12
+ * Snake_case identifier for a US State or Territory.
13
+ *
14
+ * Loosely typed as `string` for now. The Nexus original derived this from `StateName` (via `@isp.nexus/tiger`); this
15
+ * port drops that dependency since 2a doesn't consume the literal union anywhere. Task 2c tightens this against
16
+ * `@mailwoman/tiger`'s `StateName` if a downstream dictionary needs it.
17
+ *
18
+ * @category FCC
19
+ * @title FCC State Identifier
20
+ */
21
+ export type FCCStateID = string
22
+
23
+ /**
24
+ * Code indicating confidence in the association between the textual address and the location.
25
+ *
26
+ * @category NTIA
27
+ * @category FCC
28
+ */
29
+ export const AddressConfidenceCode = {
30
+ High: "1",
31
+ Medium: "2",
32
+ Low: "3",
33
+ SuccessfulChallenge: "4",
34
+ } as const
35
+
36
+ /**
37
+ * @category NTIA
38
+ * @category FCC
39
+ */
40
+ export type AddressConfidenceCode = (typeof AddressConfidenceCode)[keyof typeof AddressConfidenceCode]
41
+
42
+ /**
43
+ * A modeled land use designation estimated from assembled county assessor information.
44
+ *
45
+ * @category NTIA
46
+ * @category FCC
47
+ */
48
+ export const LandUseCode = {
49
+ Residential: 1,
50
+ Land: 2,
51
+ Business: 3,
52
+ Unknown: 4,
53
+ Agriculture: 5,
54
+ Community: 6,
55
+ Industrial: 7,
56
+ Recreation: 8,
57
+ Utility: 9,
58
+ MixedUse: 10,
59
+ Transportation: 11,
60
+ Water: 12,
61
+ Communications: 13,
62
+ Row: 14,
63
+ Wireless: 15,
64
+ Other: 0,
65
+ } as const
66
+
67
+ /**
68
+ * @category NTIA
69
+ * @category FCC
70
+ */
71
+ export type LandUseCode = (typeof LandUseCode)[keyof typeof LandUseCode]
72
+
73
+ /**
74
+ * Flag indicating if the location is a broadband serviceable location.
75
+ */
76
+ export const BSLFlag = {
77
+ Serviceable: 1,
78
+ NotServiceable: 0,
79
+ } as const
80
+
81
+ export type BSLFlag = (typeof BSLFlag)[keyof typeof BSLFlag]
82
+
83
+ /**
84
+ * Interface representing a record from the NTIA data dictionary.
85
+ *
86
+ * @category NTIA
87
+ * @title NTIA Record
88
+ */
89
+ export interface NTIARecord {
90
+ /**
91
+ * Unique ID for the Fabric location. Remains persistent across versions, even if the location's position or building
92
+ * changes.
93
+ *
94
+ * @title Location ID
95
+ * @pattern ^\d{10}$
96
+ */
97
+ location_id: BroadbandServicableLocationID
98
+
99
+ /**
100
+ * Primary postal address excluding city, state, and ZIP code.
101
+ *
102
+ * @example
103
+ * 123 Main St
104
+ *
105
+ * @title Primary Address
106
+ */
107
+ address_primary: string
108
+
109
+ /**
110
+ * City name from the postal address. Also known as the locality.
111
+ *
112
+ * @example
113
+ * San Francisco
114
+ *
115
+ * @title City
116
+ */
117
+ city: string
118
+
119
+ /**
120
+ * 2-character state abbreviation from the postal address.
121
+ *
122
+ * Loosely typed as `string` — see {@linkcode FCCStateID} for why the tiger-backed literal union is deferred to task
123
+ * 2c.
124
+ *
125
+ * @example
126
+ * CA
127
+ *
128
+ * @title State Abbreviation
129
+ */
130
+ state: string
131
+
132
+ /**
133
+ * 5-digit ZIP code associated with the address.
134
+ *
135
+ * @example
136
+ * 94103
137
+ *
138
+ * @title ZIP Code
139
+ */
140
+ zip: string
141
+
142
+ /**
143
+ * USPS ZIP+4 extension.
144
+ *
145
+ * @example
146
+ * e.g. 94103-1234
147
+ *
148
+ * @title ZIP+4
149
+ */
150
+ zip_suffix?: string
151
+
152
+ /**
153
+ * Estimate of the number of units at the location. Includes both residential and non-residential units.
154
+ *
155
+ * @example
156
+ * 10
157
+ *
158
+ * @title Unit Count
159
+ */
160
+ unit_count: number
161
+
162
+ /**
163
+ * Flag indicating if the location is a broadband serviceable location. 1 for serviceable, 0 for not.
164
+ *
165
+ * @example
166
+ * 1
167
+ *
168
+ * @title BSL Flag
169
+ */
170
+ bsl_flag: BSLFlag
171
+
172
+ /**
173
+ * Code indicating the type of building at the location. Can be residential (R), non-residential (B), mixed (X), group
174
+ * quarters (G), CAI (C), enterprise (E), or other (O).
175
+ *
176
+ * @example
177
+ * R
178
+ *
179
+ * @title Building Type Code
180
+ */
181
+ building_type_code: BuildingTypeCode
182
+
183
+ /**
184
+ * Modeled land use designation from county assessor information. Possible values range from 0 (Other) to 15
185
+ * (Wireless).
186
+ *
187
+ * @example
188
+ * 1
189
+ *
190
+ * @title Land Use Code
191
+ */
192
+ land_use_code: LandUseCode | null
193
+
194
+ /**
195
+ * Code indicating confidence in the association between the textual address and the location. 1 = High, 2 = Medium, 3
196
+ * = Low, 4 = successful address challenge.
197
+ *
198
+ * @example
199
+ * 1
200
+ *
201
+ * @title Address Confidence Code
202
+ */
203
+ address_confidence_code: AddressConfidenceCode
204
+
205
+ /**
206
+ * 5-digit TIGER 2020 identifier for the county based on the latitude and longitude.
207
+ *
208
+ * @example
209
+ * 06075
210
+ *
211
+ * @title County GEOID
212
+ */
213
+ county_geoid: string
214
+
215
+ /**
216
+ * 15-digit 2020 U.S. Census Bureau FIPS code for the census block, based on the latitude and longitude.
217
+ *
218
+ * @example
219
+ * 060750001000123
220
+ *
221
+ * @title Block GEOID
222
+ */
223
+ block_geoid: string
224
+
225
+ /**
226
+ * H3 hex cell ID, level 9, for geospatial referencing.
227
+ *
228
+ * @example
229
+ * 8928308280fffff
230
+ *
231
+ * @title H3 Cell ID (9)
232
+ */
233
+ h3_9: string
234
+
235
+ /**
236
+ * Latitude coordinate of the location in decimal degrees. Uses WGS84 format with 5-digit precision.
237
+ *
238
+ * @example
239
+ * 37.774929
240
+ *
241
+ * @minimum -90
242
+ * @maximum 90
243
+ * @title Latitude
244
+ */
245
+ latitude: number
246
+
247
+ /**
248
+ * Longitude coordinate of the location in decimal degrees. Uses WGS84 format with 5-digit precision.
249
+ *
250
+ * @example
251
+ * e.g. -122.419416
252
+ *
253
+ * @minimum -180
254
+ * @maximum 180
255
+ * @title Longitude
256
+ */
257
+ longitude: number
258
+
259
+ /**
260
+ * FCC Fabric Release Date, present only for tier 2, 3, and 4 licensees.
261
+ *
262
+ * @example
263
+ * 07212023
264
+ *
265
+ * @title FCC Fabric Release Date
266
+ */
267
+ fcc_rel?: string
268
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ * @file FCC BDC availability-file download + zip extraction.
6
+ *
7
+ * Re-homed from Nexus's `sync/fcc/bdc/download-file.ts` (relicense-by-copy, no provenance headers),
8
+ * trimmed hard: the Nexus original downloaded AND cached the `.zip`, extracted it, THEN wrote a Parquet
9
+ * file with a row-count integrity check. All Parquet machinery is dropped here — 2a's `bdc.db` is
10
+ * SQLite, not Parquet-backed (see Task 7+) — and the `.zip` itself isn't cached either; only the
11
+ * extracted CSV is written to `destinationDir`, and its presence alone is the cache check.
12
+ *
13
+ * The zip-extraction library also changes: the Nexus original's `extractSingleFileZip` used `adm-zip`
14
+ * (a repo-wide Nexus dependency). No unzip dependency exists anywhere in this repo — every workspace
15
+ * `package.json` was checked, `tiger/` and `osm/` included — so `yauzl-promise` is added to `bdc` alone
16
+ * (noted in this task's commit body per the brief).
17
+ */
18
+
19
+ import * as fs from "node:fs/promises"
20
+ import * as path from "node:path"
21
+
22
+ import { fromBuffer } from "yauzl-promise"
23
+
24
+ import type { BDCClient } from "./client.ts"
25
+ import { BDCFilingDataType, type BDCFile } from "./common.ts"
26
+
27
+ /**
28
+ * Extract the first file entry of a zip archive buffer into a single in-memory `Buffer`.
29
+ *
30
+ * BDC availability downloads are always a single zip-wrapped CSV, so — like the Nexus original — this doesn't walk
31
+ * every entry, just the first non-directory one.
32
+ */
33
+ async function extractSingleFileZip(zippedBuffer: Buffer): Promise<Buffer> {
34
+ const zip = await fromBuffer(zippedBuffer)
35
+
36
+ try {
37
+ for await (const entry of zip) {
38
+ if (entry.filename.endsWith("/")) continue
39
+
40
+ const readStream = await entry.openReadStream()
41
+ const chunks: Buffer[] = []
42
+
43
+ for await (const chunk of readStream) {
44
+ chunks.push(chunk)
45
+ }
46
+
47
+ return Buffer.concat(chunks)
48
+ }
49
+
50
+ throw new Error("extractSingleFileZip: no file entries found in zip archive.")
51
+ } finally {
52
+ await zip.close()
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Download and cache an FCC BDC availability file, extracting its zip-wrapped CSV to `destinationDir`.
58
+ *
59
+ * Cache-if-exists: if the extracted CSV already exists at the destination path, this returns immediately without
60
+ * issuing any network request. Otherwise it downloads the zip via `client`, extracts the CSV, writes it to
61
+ * `destinationDir`, and returns the written path. Only the extracted CSV is ever cached — the intermediate `.zip` is
62
+ * never written to disk.
63
+ *
64
+ * THIS FILE OWNS THE CACHE FOR THE DOWNLOAD PATH, which is why `BDCClient.getArrayBuffer` switches the client's own
65
+ * response cache off: the `existsSync`-equivalent check above is the real cache hit, and running a
66
+ * multi-hundred-megabyte archive through a JSON-validating disk cache would write a second, unreadable copy of a file
67
+ * already on disk here.
68
+ *
69
+ * @returns The path of the extracted (and now cached) CSV file.
70
+ */
71
+ export async function downloadBDCFile(client: BDCClient, file: BDCFile, destinationDir: string): Promise<string> {
72
+ const csvPath = path.join(destinationDir, `${file.fileName}.csv`)
73
+
74
+ const alreadyCached = await fs
75
+ .access(csvPath)
76
+ .then(() => true)
77
+ .catch(() => false)
78
+
79
+ if (alreadyCached) return csvPath
80
+
81
+ const zippedArrayBuffer = await client.getArrayBuffer(
82
+ `/map/downloads/downloadFile/${BDCFilingDataType.Availability}/${file.fileID}`
83
+ )
84
+
85
+ const csvBuffer = await extractSingleFileZip(Buffer.from(zippedArrayBuffer))
86
+
87
+ await fs.mkdir(destinationDir, { recursive: true })
88
+ await fs.writeFile(csvPath, csvBuffer)
89
+
90
+ return csvPath
91
+ }