@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/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # @mailwoman/bdc
2
+
3
+ FCC Broadband Data Collection (BDC) availability data — fetch, parse, and ingest provider data, and the `bdc.db` layer reader. See the implementation plan at `.superpowers/sdd/2026-07-30-bdc-2a-plan/` for the spec. This workspace is a **data provider** — it reads/ingests raw BDC data and produces a queryable spatial layer; it is not part of the address parser itself.
4
+
5
+ ## CostQuest Fabric boundary
6
+
7
+ The BDC Fabric (the BSL `location_id` → rooftop point/parcel map) is CostQuest-licensed. This workspace never ingests, ships, or derives data from the Fabric: `location_id` is carried only as an opaque join key that a licensed user may join against their own Fabric copy. All spatial work happens at census-block granularity plus mailwoman's own address spine.
package/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./schema.ts"
2
+ export * from "./sdk/index.ts"
package/out/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./schema.ts";
2
+ export * from "./sdk/index.ts";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA"}
package/out/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./schema.js";
2
+ export * from "./sdk/index.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Typed schema for bdc.db — the FCC Broadband Data Collection availability read-side layer (2a
7
+ * decisions 4, 8). `bdc_availability` holds, in the default (non-`includeLocationIDs`) build mode, one
8
+ * row per DISTINCT (geoid, provider_id, technology_code, max_advertised_download_speed,
9
+ * max_advertised_upload_speed, low_latency, business_residential_code) tuple parsed from the FCC's
10
+ * per-provider CSV (see `sdk/parsing.ts`) — NOT one row per (block, provider, technology) triple: when
11
+ * two Broadband Serviceable Locations in the same block file different speeds/flags for the same
12
+ * provider/technology, both rows survive `build-bdc.ts`'s materialize-time collapse (see that file's
13
+ * docstring; accepted FCC filing behavior, not a bug). `bdc_provider` is a small dictionary
14
+ * keyed on `provider_id`, populated by a later registry-join task — decision 8 keeps FRN/brand/
15
+ * holding-company resolution out of 2a's scope. The DB also embeds the layer-contract tables from
16
+ * `@mailwoman/core/layers` (manifest tier `shipped`, license `public-domain` — FCC BDC block-level
17
+ * availability data, at the granularity this layer ships, is US government public-domain data, not
18
+ * redistribution-restricted; the CostQuest Fabric boundary this workspace never crosses is the
19
+ * licensing edge, see `bdc/README.md` — spine `h3` res 9 for availability rows, res 6 for coverage
20
+ * cells, matching poi.db's convention).
21
+ *
22
+ * Clustering decision (implementer's pick — the brief allows either): a PLAIN rowid table, NOT
23
+ * `WITHOUT ROWID`, and NOT a composite `(h3_cell, provider_id, technology_code)` primary key.
24
+ * `WITHOUT ROWID` earns its keep on small, PK-probed rows — poi.db's clustered key and
25
+ * `layer_coverage`'s per-cell probe both read by their exact PK and nothing else, so folding the row
26
+ * into the B-tree removes a second lookup. `bdc_availability` doesn't fit that shape: it's a wider,
27
+ * 10-column row read two different ways — an h3-range scan ("everything near this cell", the
28
+ * coverage/overlay path) and a geoid point lookup (the public spatial join key TIGER and callers
29
+ * actually probe by) — never a single composite-key point probe. Clustering the full row under a
30
+ * composite PK would also force the bulk loader to sort-then-insert in that exact key order across
31
+ * millions of rows spread over per-provider, per-state source files, for a locality win a plain
32
+ * index already delivers without constraining ingest order. So: a regular rowid table, an index on
33
+ * `h3_cell` bundled into {@link createBDCAvailabilityTable} (every reader depends on it, same as the
34
+ * table itself), and {@link createBDCGeoidIndex} as a separate, callable-after-bulk-load secondary
35
+ * index for the geoid path — the index-after-load discipline used elsewhere in this repo (see
36
+ * poi-schema.ts's name_key/brand indexes).
37
+ */
38
+ import type { LayerContractDatabase } from "@mailwoman/core/layers";
39
+ import type { Kysely } from "kysely";
40
+ /**
41
+ * One availability row from the FCC's per-provider BDC CSV. In the default build mode this is one row per DISTINCT
42
+ * (geoid, provider_id, technology_code, speeds, low_latency, business_residential_code) tuple — NOT one row per (block,
43
+ * provider, technology) triple; a triple whose BSLs carry differing speed tiers keeps multiple rows here (see
44
+ * `build-bdc.ts`'s docstring).
45
+ */
46
+ export interface BDCAvailabilityTable {
47
+ /**
48
+ * Res-9 integer short H3 cell of the block centroid (spine).
49
+ */
50
+ h3_cell: number;
51
+ /**
52
+ * 15-char census block GEOID (public spatial key).
53
+ */
54
+ geoid: string;
55
+ /**
56
+ * Block-centroid PIP at build time (spine).
57
+ */
58
+ wof_id: number | null;
59
+ provider_id: number;
60
+ technology_code: number;
61
+ max_advertised_download_speed: number;
62
+ max_advertised_upload_speed: number;
63
+ low_latency: 0 | 1;
64
+ business_residential_code: string;
65
+ /**
66
+ * Opaque BSL join key — NEVER resolved (spec §2.2); null unless `--include-location-ids`.
67
+ */
68
+ location_id: string | null;
69
+ }
70
+ /**
71
+ * Provider dictionary keyed on `provider_id`. Populated by a later registry-join task (2a decision 8) — that task is 3a
72
+ * Task 8's optional `BuildBDCOptions.providers` (`bdc/sdk/build-bdc.ts`'s `populateBDCProviderTable`); when that option
73
+ * is omitted (the default), this table stays empty, exactly as it did before Task 8. No FK constraint against
74
+ * `bdc_availability.provider_id` — SQLite doesn't enforce FKs without `PRAGMA foreign_keys`, and the join happens at
75
+ * read time, not write time.
76
+ *
77
+ * **Decision 6 — this table is an explicitly LOSSY denormalization, not the source of truth.** `provider_id` is the PK
78
+ * (one row per provider), but the FCC's BDC provider list lets one `provider_id` carry MULTIPLE `frn` values — and
79
+ * conflicting `holding_company` strings — across its rows (Task 3's `parseProviderList` preserves every one of them;
80
+ * see `filer/sdk/provider-list.ts`). A single-row-per-provider table cannot express that cardinality. `filer.db`
81
+ * (`@mailwoman/filer`) is the source of truth: it retains every `provider_id`↔`frn` (and
82
+ * `provider_id`↔`holding_company_name`) edge, never folded or last-wins. When `bdc.db` is built with
83
+ * `BuildBDCOptions.providers` supplied:
84
+ *
85
+ * - `frn` holds only the PRIMARY FRN — the one carrying the most recent Form 499 filing date, per
86
+ * `@mailwoman/filer/sdk`'s `readFRNFilingCandidates` + `pickPrimaryFRN` (imported into `build-bdc.ts`, never
87
+ * reimplemented — Task 7's review found and fixed a temporal bug in that exact query, one a fresh implementation
88
+ * would reintroduce). Every OTHER FRN that `provider_id` carries is discarded here but stays fully recoverable from
89
+ * `filer.db`.
90
+ * - `holding_company` gets the SAME single-distinct-value shortcut `frn` gets (review fix round 1, IMPORTANT-3): when a
91
+ * `provider_id`'s rows carry exactly one distinct non-null `holding_company` string, there is no conflict to resolve,
92
+ * so it's populated directly — no rule needed, same as a single-FRN provider needs no `filerDB` query. When they
93
+ * carry MORE than one distinct value, that ambiguity is the real conflict decision 6 refuses to paper over with
94
+ * last-wins (`holding_company` has no most-recent-filing-date rule the way `frn` does), so it stays NULL and every
95
+ * discarded value remains recoverable from `filer.db`'s `holding_company_name` edges — the identical discipline
96
+ * `frn`'s primary pick already applies.
97
+ * - `brand_name` stays NULL unconditionally: the provider list carries no brand-name column at all, primary or otherwise,
98
+ * so there is nothing to populate it from.
99
+ */
100
+ export interface BDCProviderTable {
101
+ /**
102
+ * PK.
103
+ */
104
+ provider_id: number;
105
+ frn: string | null;
106
+ brand_name: string | null;
107
+ holding_company: string | null;
108
+ }
109
+ export interface BDCDatabase extends LayerContractDatabase {
110
+ bdc_availability: BDCAvailabilityTable;
111
+ bdc_provider: BDCProviderTable;
112
+ }
113
+ /**
114
+ * H3 resolution for `bdc_availability.h3_cell` — the block-centroid spine resolution.
115
+ */
116
+ export declare const BDC_H3_RESOLUTION = 9;
117
+ /**
118
+ * H3 resolution for `layer_coverage.h3_cell` in bdc.db — coarser than the availability spine, matching poi.db's res-6
119
+ * coverage-cell convention.
120
+ */
121
+ export declare const BDC_COVERAGE_H3_RESOLUTION = 6;
122
+ /**
123
+ * Create `bdc_availability` as a plain rowid table, plus the `h3_cell` range-scan index (bundled here since every
124
+ * reader depends on it — see the file header for the clustering rationale). Call {@link createBDCGeoidIndex}
125
+ * separately, after bulk load, for the geoid point-lookup path.
126
+ */
127
+ export declare function createBDCAvailabilityTable(db: Kysely<BDCDatabase>): Promise<void>;
128
+ /**
129
+ * Create `bdc_provider`. A single-column INTEGER PRIMARY KEY is already the SQLite rowid alias, so there's no `WITHOUT
130
+ * ROWID` win here — that modifier only pays off clustering a composite or non-integer PK.
131
+ */
132
+ export declare function createBDCProviderTable(db: Kysely<BDCDatabase>): Promise<void>;
133
+ /**
134
+ * Secondary index for the geoid point-lookup path (the public spatial join key). Call AFTER the bulk materialize
135
+ * (index-after-load), same discipline as poi.db's secondary indexes.
136
+ */
137
+ export declare function createBDCGeoidIndex(db: Kysely<BDCDatabase>): Promise<void>;
138
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AACnE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAEpC;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACpC;;OAEG;IACH,OAAO,EAAE,MAAM,CAAA;IACf;;OAEG;IACH,KAAK,EAAE,MAAM,CAAA;IACb;;OAEG;IACH,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,WAAW,EAAE,MAAM,CAAA;IACnB,eAAe,EAAE,MAAM,CAAA;IACvB,6BAA6B,EAAE,MAAM,CAAA;IACrC,2BAA2B,EAAE,MAAM,CAAA;IACnC,WAAW,EAAE,CAAC,GAAG,CAAC,CAAA;IAClB,yBAAyB,EAAE,MAAM,CAAA;IACjC;;OAEG;IACH,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,WAAW,gBAAgB;IAChC;;OAEG;IACH,WAAW,EAAE,MAAM,CAAA;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAED,MAAM,WAAW,WAAY,SAAQ,qBAAqB;IACzD,gBAAgB,EAAE,oBAAoB,CAAA;IACtC,YAAY,EAAE,gBAAgB,CAAA;CAC9B;AAED;;GAEG;AACH,eAAO,MAAM,iBAAiB,IAAI,CAAA;AAElC;;;GAGG;AACH,eAAO,MAAM,0BAA0B,IAAI,CAAA;AAE3C;;;;GAIG;AACH,wBAAsB,0BAA0B,CAAC,EAAE,EAAE,MAAM,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBvF;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,EAAE,EAAE,MAAM,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAQnF;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,MAAM,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhF"}
package/out/schema.js ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Typed schema for bdc.db — the FCC Broadband Data Collection availability read-side layer (2a
7
+ * decisions 4, 8). `bdc_availability` holds, in the default (non-`includeLocationIDs`) build mode, one
8
+ * row per DISTINCT (geoid, provider_id, technology_code, max_advertised_download_speed,
9
+ * max_advertised_upload_speed, low_latency, business_residential_code) tuple parsed from the FCC's
10
+ * per-provider CSV (see `sdk/parsing.ts`) — NOT one row per (block, provider, technology) triple: when
11
+ * two Broadband Serviceable Locations in the same block file different speeds/flags for the same
12
+ * provider/technology, both rows survive `build-bdc.ts`'s materialize-time collapse (see that file's
13
+ * docstring; accepted FCC filing behavior, not a bug). `bdc_provider` is a small dictionary
14
+ * keyed on `provider_id`, populated by a later registry-join task — decision 8 keeps FRN/brand/
15
+ * holding-company resolution out of 2a's scope. The DB also embeds the layer-contract tables from
16
+ * `@mailwoman/core/layers` (manifest tier `shipped`, license `public-domain` — FCC BDC block-level
17
+ * availability data, at the granularity this layer ships, is US government public-domain data, not
18
+ * redistribution-restricted; the CostQuest Fabric boundary this workspace never crosses is the
19
+ * licensing edge, see `bdc/README.md` — spine `h3` res 9 for availability rows, res 6 for coverage
20
+ * cells, matching poi.db's convention).
21
+ *
22
+ * Clustering decision (implementer's pick — the brief allows either): a PLAIN rowid table, NOT
23
+ * `WITHOUT ROWID`, and NOT a composite `(h3_cell, provider_id, technology_code)` primary key.
24
+ * `WITHOUT ROWID` earns its keep on small, PK-probed rows — poi.db's clustered key and
25
+ * `layer_coverage`'s per-cell probe both read by their exact PK and nothing else, so folding the row
26
+ * into the B-tree removes a second lookup. `bdc_availability` doesn't fit that shape: it's a wider,
27
+ * 10-column row read two different ways — an h3-range scan ("everything near this cell", the
28
+ * coverage/overlay path) and a geoid point lookup (the public spatial join key TIGER and callers
29
+ * actually probe by) — never a single composite-key point probe. Clustering the full row under a
30
+ * composite PK would also force the bulk loader to sort-then-insert in that exact key order across
31
+ * millions of rows spread over per-provider, per-state source files, for a locality win a plain
32
+ * index already delivers without constraining ingest order. So: a regular rowid table, an index on
33
+ * `h3_cell` bundled into {@link createBDCAvailabilityTable} (every reader depends on it, same as the
34
+ * table itself), and {@link createBDCGeoidIndex} as a separate, callable-after-bulk-load secondary
35
+ * index for the geoid path — the index-after-load discipline used elsewhere in this repo (see
36
+ * poi-schema.ts's name_key/brand indexes).
37
+ */
38
+ /**
39
+ * H3 resolution for `bdc_availability.h3_cell` — the block-centroid spine resolution.
40
+ */
41
+ export const BDC_H3_RESOLUTION = 9;
42
+ /**
43
+ * H3 resolution for `layer_coverage.h3_cell` in bdc.db — coarser than the availability spine, matching poi.db's res-6
44
+ * coverage-cell convention.
45
+ */
46
+ export const BDC_COVERAGE_H3_RESOLUTION = 6;
47
+ /**
48
+ * Create `bdc_availability` as a plain rowid table, plus the `h3_cell` range-scan index (bundled here since every
49
+ * reader depends on it — see the file header for the clustering rationale). Call {@link createBDCGeoidIndex}
50
+ * separately, after bulk load, for the geoid point-lookup path.
51
+ */
52
+ export async function createBDCAvailabilityTable(db) {
53
+ await db.schema
54
+ .createTable("bdc_availability")
55
+ .addColumn("h3_cell", "integer", (c) => c.notNull())
56
+ .addColumn("geoid", "text", (c) => c.notNull())
57
+ .addColumn("wof_id", "integer")
58
+ .addColumn("provider_id", "integer", (c) => c.notNull())
59
+ .addColumn("technology_code", "integer", (c) => c.notNull())
60
+ .addColumn("max_advertised_download_speed", "integer", (c) => c.notNull())
61
+ .addColumn("max_advertised_upload_speed", "integer", (c) => c.notNull())
62
+ .addColumn("low_latency", "integer", (c) => c.notNull())
63
+ .addColumn("business_residential_code", "text", (c) => c.notNull())
64
+ .addColumn("location_id", "text")
65
+ .execute();
66
+ await db.schema.createIndex("bdc_availability_h3_cell").on("bdc_availability").column("h3_cell").execute();
67
+ }
68
+ /**
69
+ * Create `bdc_provider`. A single-column INTEGER PRIMARY KEY is already the SQLite rowid alias, so there's no `WITHOUT
70
+ * ROWID` win here — that modifier only pays off clustering a composite or non-integer PK.
71
+ */
72
+ export async function createBDCProviderTable(db) {
73
+ await db.schema
74
+ .createTable("bdc_provider")
75
+ .addColumn("provider_id", "integer", (c) => c.primaryKey())
76
+ .addColumn("frn", "text")
77
+ .addColumn("brand_name", "text")
78
+ .addColumn("holding_company", "text")
79
+ .execute();
80
+ }
81
+ /**
82
+ * Secondary index for the geoid point-lookup path (the public spatial join key). Call AFTER the bulk materialize
83
+ * (index-after-load), same discipline as poi.db's secondary indexes.
84
+ */
85
+ export async function createBDCGeoidIndex(db) {
86
+ await db.schema.createIndex("bdc_availability_geoid").on("bdc_availability").column("geoid").execute();
87
+ }
88
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAiFH;;GAEG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAA;AAElC;;;GAGG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAA;AAE3C;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAAC,EAAuB;IACvE,MAAM,EAAE,CAAC,MAAM;SACb,WAAW,CAAC,kBAAkB,CAAC;SAC/B,SAAS,CAAC,SAAS,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;SACnD,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;SAC9C,SAAS,CAAC,QAAQ,EAAE,SAAS,CAAC;SAC9B,SAAS,CAAC,aAAa,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;SACvD,SAAS,CAAC,iBAAiB,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;SAC3D,SAAS,CAAC,+BAA+B,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;SACzE,SAAS,CAAC,6BAA6B,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;SACvE,SAAS,CAAC,aAAa,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;SACvD,SAAS,CAAC,2BAA2B,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;SAClE,SAAS,CAAC,aAAa,EAAE,MAAM,CAAC;SAChC,OAAO,EAAE,CAAA;IAEX,MAAM,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,0BAA0B,CAAC,CAAC,EAAE,CAAC,kBAAkB,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAA;AAC3G,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,EAAuB;IACnE,MAAM,EAAE,CAAC,MAAM;SACb,WAAW,CAAC,cAAc,CAAC;SAC3B,SAAS,CAAC,aAAa,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC;SAC1D,SAAS,CAAC,KAAK,EAAE,MAAM,CAAC;SACxB,SAAS,CAAC,YAAY,EAAE,MAAM,CAAC;SAC/B,SAAS,CAAC,iBAAiB,EAAE,MAAM,CAAC;SACpC,OAAO,EAAE,CAAA;AACZ,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,EAAuB;IAChE,MAAM,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,wBAAwB,CAAC,CAAC,EAAE,CAAC,kBAAkB,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,CAAA;AACvG,CAAC"}
@@ -0,0 +1,214 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The `bdc.db` builder (2a Task 8) — ingests parsed FCC BDC availability rows (Task 6's
7
+ * {@link BDCAvailabilityRow}) into Task 7's schema (`schema.ts`), producing a sealed layer database.
8
+ *
9
+ * Mirrors `mailwoman/gazetteer-pipeline/poi/build-poi.ts`'s shape closely: the same build-tuning
10
+ * pragmas, the same single-pass `Map<number, number>` coverage aggregation taken during the load
11
+ * (no second scan), the same {@link asContractDB} Kysely-invariance cast for the shared
12
+ * `@mailwoman/core/layers` calls, and the same `writeLayerManifest` → `sealDatabase` tail.
13
+ *
14
+ * Two differences from that precedent, both deliberate:
15
+ *
16
+ * 1. **A staging-dedup pass.** BDC's per-provider CSVs can carry exact-duplicate rows (repeat
17
+ * filings, overlapping re-downloads); poi's Overture rows never needed this. `bdc_stage` is a
18
+ * plain Kysely-built table (not part of the public {@link BDCDatabase} — it never survives to the
19
+ * sealed artifact) carrying a composite PRIMARY KEY on the natural key
20
+ * `(geoid, provider_id, technology_code, location_id)`. Rows load via a RAW prepared
21
+ * `INSERT OR IGNORE` — the AGENTS.md "hot bulk write" carve-out, same discipline as the
22
+ * candidate-gazetteer builder — which is the direct replacement for the Redis
23
+ * set-membership check Nexus's `sync/commands/bdc/infer-locations.ts` used for this exact
24
+ * dedup (dedup semantics only; Nexus's Redis-backed location inference itself has no analog
25
+ * here). `h3_cell` is computed only AFTER staging, per distinct geoid, against the deduped set —
26
+ * so a duplicate row is never charged twice against `unknownGeoids` or `layer_coverage` either.
27
+ * The natural key's `location_id` component means the SAME (geoid, provider_id, technology_code)
28
+ * triple legitimately survives staging once per distinct BSL — correct when `includeLocationIDs`
29
+ * is true. The default (NULL `location_id`) mode collapses at MATERIALIZE time via `SELECT DISTINCT`
30
+ * over every column EXCEPT `location_id` — that is, to one row per distinct (geoid, provider_id,
31
+ * technology_code, max_advertised_download_speed, max_advertised_upload_speed, low_latency,
32
+ * business_residential_code) TUPLE, NOT one row per (geoid, provider_id, technology_code) triple.
33
+ * When every BSL in a block shares identical speeds/flags for a given provider/technology (the
34
+ * common case), those two are the same thing and the collapse yields exactly one row per triple.
35
+ * But when BSLs at the SAME triple carry DIFFERENT speeds/flags (a real, accepted FCC filing
36
+ * pattern — a provider filing different advertised speeds at different addresses in one block),
37
+ * the distinct rows survive collapse as multiple NULL-`location_id` rows at that one triple. This
38
+ * is deliberate, matches the FCC source data's own granularity, and is NOT a bug to fix — see
39
+ * `filing-landscape.ts`'s module docstring for the read-side consequence (the same provider/tech
40
+ * can surface in more than one `speed_bucket` for one queried block).
41
+ * 2. **Temp-path build + move-aside-first swap**, per AGENTS.md's database house rule ("build
42
+ * successfully, then move the previous version to a temp directory, and then move the new
43
+ * version into place... ensures the database is always in a consistent state, even if the build
44
+ * script fails halfway through"): build lands at `${out}.building`, seals there, then
45
+ * `${out}` (if present) is renamed to `${out}.prev` BEFORE the sealed build takes its place —
46
+ * mirroring `mailwoman/eval-harness/gauntlet/build-regression-db.ts`'s swap. `build-poi.ts`
47
+ * instead writes `out` directly (removing any stale file first) and records ITS OWN deviation
48
+ * from an even older staging-suffix convention — see that file's docstring. This builder takes
49
+ * the opposite fork on purpose: a from-scratch nationwide BDC ingest is long enough that a
50
+ * mid-build crash mustn't cost the previously-good artifact, which is exactly the failure mode
51
+ * the house rule exists for.
52
+ */
53
+ import { DatabaseClient } from "@mailwoman/core/kysley/client";
54
+ import type { FilerDatabase } from "@mailwoman/filer";
55
+ import type { ProviderListRow } from "@mailwoman/filer/sdk";
56
+ import type { ProviderID } from "./common.ts";
57
+ import { type BDCAvailabilityRow } from "./parsing.ts";
58
+ /**
59
+ * The manifest's `attribution` — names the FCC as the source, then copies the Fabric-boundary sentence verbatim from
60
+ * `bdc/README.md`'s "CostQuest Fabric boundary" section (backticks stripped — this is plain prose, not markdown).
61
+ */
62
+ export declare const BDC_ATTRIBUTION: string;
63
+ export interface BuildBDCOptions {
64
+ /**
65
+ * Injected row source — the test seam (mirrors `BuildPOIOptions.rows`). When given, `csvPaths` is ignored and no
66
+ * filesystem read happens.
67
+ */
68
+ rows?: Iterable<BDCAvailabilityRow> | AsyncIterable<BDCAvailabilityRow>;
69
+ /**
70
+ * Per-provider availability CSVs for one state (as extracted by `downloadBDCFile`). Ignored when `rows` is given.
71
+ * Required unless `rows` is given. Each file's constant `provider_id` column is peeked off its first data row (see
72
+ * {@linkcode peekProviderID}) rather than threaded through as a parallel array — the FCC's per-provider files already
73
+ * carry it once per file, redundantly, in column 1.
74
+ */
75
+ csvPaths?: string[];
76
+ /**
77
+ * Output `bdc.db` path. Built at `${out}.building` and moved into place last — see the module docstring.
78
+ */
79
+ out: string;
80
+ /**
81
+ * The FCC filing's `as_of_date` (e.g. from Task 5's `resolveLatestVintage`) — becomes the manifest's `sourceVintage`
82
+ * AND `version` (BDC has no independent layer versioning yet, same deferral `build-poi.ts` makes for `release`).
83
+ */
84
+ asOfDate: string;
85
+ /**
86
+ * `git rev-parse --short HEAD` — passed in by the command, not read from the repo here.
87
+ */
88
+ buildSHA: string;
89
+ /**
90
+ * Populate `bdc_availability.location_id` (the opaque BSL join key — spec §2.2, NEVER resolved against the Fabric).
91
+ * Default `false`: the column stays `NULL` unless a caller explicitly opts in.
92
+ */
93
+ includeLocationIDs?: boolean;
94
+ /**
95
+ * Resolve a 15-char census block GEOID to its centroid. Injected so tests supply a small fixture `Map` lookup instead
96
+ * of touching a real TIGER database; the real (CLI-wired) implementation is
97
+ * {@linkcode createTIGERBlockCentroidLookup}, which reads `tabblock20.GEOID` (uppercase — `TIGERBlockTable`) block
98
+ * geometry. Returning `undefined` for an unknown geoid is load-bearing: the materialize pass counts it in
99
+ * `unknownGeoids` and skips the row — it must NEVER guess a cell.
100
+ */
101
+ blockCentroids: (geoid: string) => {
102
+ lat: number;
103
+ lon: number;
104
+ } | undefined;
105
+ onProgress?: (message: string) => void;
106
+ /**
107
+ * Provider-list rows (Task 3's {@link ProviderListRow}, `@mailwoman/filer/sdk`'s `parseProviderList`) — the test/CLI
108
+ * seam for populating `bdc_provider` (2a decision 8 / 3a decision 6, Task 8). When ABSENT (the default),
109
+ * `bdc_provider` stays empty and the rest of the build is byte-identical to the pre-Task-8 behavior: every code path
110
+ * this option touches is gated behind `if (options.providers)`, so omitting it changes nothing. When present,
111
+ * `buildBDCDatabase` groups rows by `providerID` and inserts one `bdc_provider` row per distinct provider — see
112
+ * {@link BuildBDCOptions.filerDB} for how the primary FRN is picked when a provider carries more than one, and
113
+ * `schema.ts`'s `BDCProviderTable` docstring for the full lossy-denormalization rationale (decision 6).
114
+ */
115
+ providers?: Iterable<ProviderListRow> | AsyncIterable<ProviderListRow>;
116
+ /**
117
+ * Filer.db handle (`@mailwoman/filer`) used to resolve a multi-FRN provider's PRIMARY FRN via
118
+ * `readFRNFilingCandidates` + `pickPrimaryFRN` (`@mailwoman/filer/sdk`, decision 6) — imported rather than
119
+ * reimplemented, since Task 7's review found and fixed a temporal bug (the candidate query originally omitted the
120
+ * `valid_to` half of the half-open predicate) that a fresh implementation here would reintroduce. Only actually
121
+ * QUERIED for a `provider_id` whose rows carry more than one distinct `frn` — a single-FRN provider needs no lookup,
122
+ * since its lone FRN is already primary by construction. Required whenever `providers` is given AND at least one
123
+ * `provider_id` turns out to be multi-FRN; `buildBDCDatabase` throws a descriptive error naming the offending
124
+ * `provider_id` if it's needed but missing, rather than silently picking an arbitrary FRN.
125
+ */
126
+ filerDB?: DatabaseClient<FilerDatabase>;
127
+ /**
128
+ * `asOf` date for the primary-FRN candidate query (`readFRNFilingCandidates`'s half-open `valid_from`/`valid_to`
129
+ * scoping — see `filer/sdk/filer-lookup.ts`). Defaults to {@link BuildBDCOptions.asOfDate} (this bdc.db build's own
130
+ * vintage) when omitted.
131
+ */
132
+ primaryFRNAsOf?: string;
133
+ }
134
+ export interface BuildBDCResult {
135
+ out: string;
136
+ /**
137
+ * Rows materialized into `bdc_availability` (post-dedup, post-unknown-geoid-skip). In the default
138
+ * (`includeLocationIDs: false`) mode this is per DISTINCT (geoid, provider_id, technology_code, speeds, low_latency,
139
+ * business_residential_code) tuple, not per BSL — multiple BSLs at the same (geoid, provider_id, technology_code)
140
+ * triple collapse to one row ONLY when their speeds/flags also match; BSLs at the same triple with differing
141
+ * speeds/flags survive as separate rows (see the module docstring).
142
+ */
143
+ rows: number;
144
+ /**
145
+ * Raw rows removed by the staging natural-key dedup (staged attempts minus distinct rows kept).
146
+ */
147
+ deduped: number;
148
+ /**
149
+ * Distinct `provider_id` values among the materialized rows.
150
+ */
151
+ providers: number;
152
+ /**
153
+ * Res-6 coverage cells written.
154
+ */
155
+ coverageCells: number;
156
+ /**
157
+ * Rows whose `geoid` had no resolvable centroid — skipped, never inserted, never guessed at a cell.
158
+ */
159
+ unknownGeoids: number;
160
+ /**
161
+ * `bdc_provider` rows written — 0 when `options.providers` was not supplied (the default path never touches this
162
+ * table, see {@link BuildBDCOptions.providers}).
163
+ */
164
+ providersPopulated: number;
165
+ }
166
+ /**
167
+ * Peek the constant `provider_id` column (index 1) off an FCC BDC availability CSV's first data row.
168
+ *
169
+ * Production per-provider files carry the SAME `provider_id` in every row (the FCC partitions availability files per
170
+ * provider) — `parsing.ts`'s `takeAvailabilityLine` already assumes this, taking `providerID` as a parameter rather
171
+ * than re-slicing column 1 per row. This reads it once, directly off the raw bytes, rather than threading a parallel
172
+ * `providerID` array alongside `csvPaths` through the public options shape.
173
+ *
174
+ * `csvPath` is optional and used ONLY to name the offending file in a thrown error (the direct-buffer unit tests call
175
+ * this without one; {@linkcode readAvailabilityRowsFromCSVPaths} always supplies it). The `Number.isSafeInteger` guard
176
+ * below is load-bearing, not defensive dressing: `bdc_stage.provider_id` is `INTEGER NOT NULL`, and a bare
177
+ * `Number.parseInt` on a non-numeric field (a malformed/re-headered/truncated CSV) silently produces `NaN`. `NaN` binds
178
+ * to that NOT NULL column as SQLite `NULL`, `INSERT OR IGNORE` then drops the row without a constraint error, and every
179
+ * dropped row gets counted as `deduped` — the ENTIRE file's rows vanish silently, misreported as ordinary dedup. A
180
+ * malformed CSV must be loud, never silently absorbed, so this throws instead.
181
+ */
182
+ export declare function peekProviderID(csvBuffer: Buffer, csvPath?: string): ProviderID;
183
+ /**
184
+ * Naive (vertex-average, NOT area-weighted) centroid of a GeoJSON `Polygon`/`MultiPolygon`'s EXTERIOR ring(s) only
185
+ * (interior rings/holes are ignored). A known simplification, not an oversight: census blocks are small relative to a
186
+ * res-9 H3 cell (~174m edge), so the vertex-average and a proper area-weighted centroid land in the same cell for all
187
+ * but pathologically elongated or holed block shapes. A precise area-weighted centroid is a reasonable future upgrade
188
+ * if that ever proves wrong in practice — no polygon-centroid library is pulled in for this first cut.
189
+ *
190
+ * Returns `undefined` for anything that doesn't parse as one of the two geometry types (including `null` geometry).
191
+ */
192
+ export declare function geometryCentroid(geometryJSON: string | null): {
193
+ lat: number;
194
+ lon: number;
195
+ } | undefined;
196
+ /**
197
+ * The production `blockCentroids` supplier: opens the TIGER blocks database READ-ONLY and probes `tabblock20.GEOID`
198
+ * (uppercase) per lookup, decoding its GeoJSON `geometry` column via {@linkcode geometryCentroid}. Kept synchronous —
199
+ * `BuildBDCOptions.blockCentroids` is a plain sync function (the same sync-by-interface discipline AGENTS.md documents
200
+ * for the resolver ladder), so this uses `node:sqlite`'s raw `.prepare()`/`.get()` directly rather than Kysely. The
201
+ * connection is left open for the caller's process lifetime (a read-path lookup, not a build) — same lifecycle as the
202
+ * resolver-wof-sqlite lookups.
203
+ */
204
+ export declare function createTIGERBlockCentroidLookup(tigerDBPath: string): (geoid: string) => {
205
+ lat: number;
206
+ lon: number;
207
+ } | undefined;
208
+ /**
209
+ * Build `bdc.db`: stage (raw dedup) → materialize (resolve `h3_cell` per geoid, skip+count unknown geoids) → drop stage
210
+ * → geoid index (index-after-load) → coverage → layer manifest → seal → atomic move-into-place. See the module
211
+ * docstring for the two deliberate deviations from `build-poi.ts`.
212
+ */
213
+ export declare function buildBDCDatabase(options: BuildBDCOptions): Promise<BuildBDCResult>;
214
+ //# sourceMappingURL=build-bdc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-bdc.d.ts","sourceRoot":"","sources":["../../sdk/build-bdc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAOH,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAW9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAKrD,OAAO,KAAK,EAAO,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAchE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EAAwB,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAQ5E;;;GAGG;AACH,eAAO,MAAM,eAAe,QAEqF,CAAA;AAEjH,MAAM,WAAW,eAAe;IAC/B;;;OAGG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAC,kBAAkB,CAAC,GAAG,aAAa,CAAC,kBAAkB,CAAC,CAAA;IACvE;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB;;OAEG;IACH,GAAG,EAAE,MAAM,CAAA;IACX;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B;;;;;;OAMG;IACH,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAA;IAC3E,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,QAAQ,CAAC,eAAe,CAAC,GAAG,aAAa,CAAC,eAAe,CAAC,CAAA;IACtE;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC,aAAa,CAAC,CAAA;IACvC;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,cAAc;IAC9B,GAAG,EAAE,MAAM,CAAA;IACX;;;;;;OAMG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ;;OAEG;IACH,OAAO,EAAE,MAAM,CAAA;IACf;;OAEG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB;;OAEG;IACH,aAAa,EAAE,MAAM,CAAA;IACrB;;OAEG;IACH,aAAa,EAAE,MAAM,CAAA;IACrB;;;OAGG;IACH,kBAAkB,EAAE,MAAM,CAAA;CAC1B;AAsDD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,UAAU,CA+B9E;AA8BD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAsCtG;AAED;;;;;;;GAOG;AACH,wBAAgB,8BAA8B,CAC7C,WAAW,EAAE,MAAM,GACjB,CAAC,KAAK,EAAE,MAAM,KAAK;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAS7D;AAmHD;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CA6RxF"}