@mailwoman/bdc 8.6.0 → 9.0.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/out/schema.d.ts +15 -17
- package/out/schema.d.ts.map +1 -1
- package/out/schema.js.map +1 -1
- package/out/sdk/build-bdc.d.ts +17 -17
- package/out/sdk/build-bdc.d.ts.map +1 -1
- package/out/sdk/build-bdc.js +26 -29
- package/out/sdk/build-bdc.js.map +1 -1
- package/out/sdk/common.d.ts +5 -5
- package/out/sdk/data-collection.d.ts +2 -2
- package/out/sdk/download.d.ts +4 -4
- package/out/sdk/download.js +4 -4
- package/out/sdk/filing-landscape.d.ts +12 -19
- package/out/sdk/filing-landscape.d.ts.map +1 -1
- package/out/sdk/filing-landscape.js +14 -22
- package/out/sdk/filing-landscape.js.map +1 -1
- package/out/sdk/list-files.d.ts +1 -1
- package/out/sdk/list-files.js +1 -1
- package/out/sdk/nearest-infrastructure.d.ts +10 -11
- package/out/sdk/nearest-infrastructure.d.ts.map +1 -1
- package/out/sdk/nearest-infrastructure.js +10 -11
- package/out/sdk/nearest-infrastructure.js.map +1 -1
- package/out/sdk/plausibility.d.ts +31 -31
- package/out/sdk/plausibility.d.ts.map +1 -1
- package/out/sdk/plausibility.js +33 -35
- package/out/sdk/plausibility.js.map +1 -1
- package/package.json +6 -6
- package/schema.ts +15 -17
- package/sdk/build-bdc.ts +42 -45
- package/sdk/common.ts +5 -5
- package/sdk/data-collection.ts +2 -2
- package/sdk/download.ts +4 -4
- package/sdk/filing-landscape.ts +14 -22
- package/sdk/list-files.ts +1 -1
- package/sdk/nearest-infrastructure.ts +10 -11
- package/sdk/plausibility.ts +43 -45
package/sdk/build-bdc.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* @license AGPL-3.0
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
5
|
*
|
|
6
|
-
* The `bdc.db` builder
|
|
7
|
-
*
|
|
6
|
+
* The `bdc.db` builder — ingests parsed FCC BDC availability rows ({@link BDCAvailabilityRow})
|
|
7
|
+
* into the schema declared in `schema.ts`, producing a sealed layer database.
|
|
8
8
|
*
|
|
9
9
|
* Mirrors `mailwoman/gazetteer-pipeline/poi/build-poi.ts`'s shape closely: the same build-tuning
|
|
10
10
|
* pragmas, the same single-pass `Map<number, number>` coverage aggregation taken during the load
|
|
@@ -66,11 +66,12 @@ import {
|
|
|
66
66
|
writeLayerManifest,
|
|
67
67
|
type LayerContractDatabase,
|
|
68
68
|
} from "@mailwoman/core/layers"
|
|
69
|
+
import { tryParsingJSON } from "@mailwoman/core/objects"
|
|
69
70
|
import { openBuiltDatabase, sealDatabase } from "@mailwoman/core/utils"
|
|
70
71
|
import type { FilerDatabase } from "@mailwoman/filer"
|
|
71
72
|
// `pickPrimaryFRN`/`readFRNFilingCandidates` are loaded via a LAZY `await import("@mailwoman/filer/sdk")`
|
|
72
73
|
// inside `populateBDCProviderTable`, not a top-level runtime import — see that function's docstring
|
|
73
|
-
|
|
74
|
+
//. Only the TYPES are imported here; `import type` is fully erased, so
|
|
74
75
|
// this line has zero runtime cost for every `@mailwoman/bdc` consumer that never populates providers.
|
|
75
76
|
import type { FRN, ProviderListRow } from "@mailwoman/filer/sdk"
|
|
76
77
|
import { shortCellToInt, type H3Cell } from "@mailwoman/spatial"
|
|
@@ -121,8 +122,8 @@ export interface BuildBDCOptions {
|
|
|
121
122
|
*/
|
|
122
123
|
out: string
|
|
123
124
|
/**
|
|
124
|
-
* The FCC filing's `as_of_date` (e.g. from
|
|
125
|
-
*
|
|
125
|
+
* The FCC filing's `as_of_date` (e.g. from `resolveLatestVintage`) — becomes the manifest's `sourceVintage` AND
|
|
126
|
+
* `version` (BDC has no independent layer versioning yet, same deferral `build-poi.ts` makes for `release`).
|
|
126
127
|
*/
|
|
127
128
|
asOfDate: string
|
|
128
129
|
/**
|
|
@@ -144,24 +145,24 @@ export interface BuildBDCOptions {
|
|
|
144
145
|
blockCentroids: (geoid: string) => { lat: number; lon: number } | undefined
|
|
145
146
|
onProgress?: (message: string) => void
|
|
146
147
|
/**
|
|
147
|
-
* Provider-list rows (
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
148
|
+
* Provider-list rows ({@link ProviderListRow}, `@mailwoman/filer/sdk`'s `parseProviderList`) — the test/CLI seam for
|
|
149
|
+
* populating `bdc_provider` (2a decision 8 / 3a decision 6). When ABSENT (the default), `bdc_provider` stays empty
|
|
150
|
+
* and the rest of the build is untouched: every code path this option touches is gated behind `if
|
|
151
|
+
* (options.providers)`, so omitting it changes nothing. When present, `buildBDCDatabase` groups rows by `providerID`
|
|
152
|
+
* and inserts one `bdc_provider` row per distinct provider — see {@link BuildBDCOptions.filerDB} for how the primary
|
|
153
|
+
* FRN is picked when a provider carries more than one, and `schema.ts`'s `BDCProviderTable` docstring for the full
|
|
154
|
+
* lossy-denormalization rationale (decision 6).
|
|
154
155
|
*/
|
|
155
156
|
providers?: Iterable<ProviderListRow> | AsyncIterable<ProviderListRow>
|
|
156
157
|
/**
|
|
157
158
|
* Filer.db handle (`@mailwoman/filer`) used to resolve a multi-FRN provider's PRIMARY FRN via
|
|
158
159
|
* `readFRNFilingCandidates` + `pickPrimaryFRN` (`@mailwoman/filer/sdk`, decision 6) — imported rather than
|
|
159
|
-
* reimplemented,
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
160
|
+
* reimplemented, because the candidate query needs BOTH halves of the half-open `valid_from`/`valid_to` predicate and
|
|
161
|
+
* a second implementation here would be a second place to drop the `valid_to` half. Only actually QUERIED for a
|
|
162
|
+
* `provider_id` whose rows carry more than one distinct `frn` — a single-FRN provider needs no lookup, since its lone
|
|
163
|
+
* FRN is already primary by construction. Required whenever `providers` is given AND at least one `provider_id` turns
|
|
164
|
+
* out to be multi-FRN; `buildBDCDatabase` throws a descriptive error naming the offending `provider_id` if it's
|
|
165
|
+
* needed but missing, rather than silently picking an arbitrary FRN.
|
|
165
166
|
*/
|
|
166
167
|
filerDB?: DatabaseClient<FilerDatabase>
|
|
167
168
|
/**
|
|
@@ -346,13 +347,9 @@ interface GeoJSONMultiPolygon {
|
|
|
346
347
|
export function geometryCentroid(geometryJSON: string | null): { lat: number; lon: number } | undefined {
|
|
347
348
|
if (!geometryJSON) return undefined
|
|
348
349
|
|
|
349
|
-
|
|
350
|
+
const geometry = tryParsingJSON<GeoJSONPolygon | GeoJSONMultiPolygon>(geometryJSON)
|
|
350
351
|
|
|
351
|
-
|
|
352
|
-
geometry = JSON.parse(geometryJSON)
|
|
353
|
-
} catch {
|
|
354
|
-
return undefined
|
|
355
|
-
}
|
|
352
|
+
if (!geometry) return undefined
|
|
356
353
|
|
|
357
354
|
const exteriorRings: number[][][] =
|
|
358
355
|
geometry.type === "Polygon"
|
|
@@ -413,8 +410,8 @@ export function createTIGERBlockCentroidLookup(
|
|
|
413
410
|
const PROVIDER_INSERT_BATCH_SIZE = 500
|
|
414
411
|
|
|
415
412
|
/**
|
|
416
|
-
* Groups `providers` by `providerID`.
|
|
417
|
-
*
|
|
413
|
+
* Groups `providers` by `providerID`. `parseProviderList` yields one {@link ProviderListRow} PER LINE of the source
|
|
414
|
+
* CSV, preserving cardinality (never folded, never last-wins — see that module's docstring) — so a `provider_id`
|
|
418
415
|
* appearing on N rows arrives here as N separate rows, exactly as decision 6 requires downstream.
|
|
419
416
|
*/
|
|
420
417
|
async function groupProviderListRows(
|
|
@@ -436,7 +433,7 @@ async function groupProviderListRows(
|
|
|
436
433
|
}
|
|
437
434
|
|
|
438
435
|
/**
|
|
439
|
-
* Populate `bdc_provider` from `options.providers` (2a decision 8 / 3a decision 6
|
|
436
|
+
* Populate `bdc_provider` from `options.providers` (2a decision 8 / 3a decision 6) — see `schema.ts`'s
|
|
440
437
|
* `BDCProviderTable` docstring for the full lossy-denormalization rationale. For each distinct `provider_id`:
|
|
441
438
|
*
|
|
442
439
|
* - Exactly one `frn` among its rows → that FRN is primary by construction; no `filerDB` query needed at all.
|
|
@@ -447,22 +444,22 @@ async function groupProviderListRows(
|
|
|
447
444
|
* checks `candidates.length` first, mirroring `filerLookup`'s own `primary_frn: null` handling of the same case.
|
|
448
445
|
* - `filerDB` is REQUIRED the instant a multi-FRN `provider_id` is encountered; its absence throws immediately, naming
|
|
449
446
|
* the offending `provider_id`, rather than silently picking an arbitrary FRN.
|
|
450
|
-
* - `holding_company` gets the IDENTICAL single-distinct-value shortcut `frn` gets
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
447
|
+
* - `holding_company` gets the IDENTICAL single-distinct-value shortcut `frn` gets: exactly one distinct non-null
|
|
448
|
+
* `holdingCompany` across a provider's rows means there's no conflict to resolve, so it's populated directly, no rule
|
|
449
|
+
* needed. Two or more distinct values IS the real conflict decision 6 refuses to paper over with last-wins — that
|
|
450
|
+
* case inserts NULL, and every value stays recoverable from `filer.db`. A `null` `holdingCompany` on some rows
|
|
451
|
+
* doesn't count as a competing value (a row simply not stating it isn't a conflicting assertion) — only distinct
|
|
452
|
+
* NON-NULL strings are compared.
|
|
456
453
|
*
|
|
457
454
|
* `brand_name` is always inserted NULL — the provider list carries no brand-name column at all, so there is nothing to
|
|
458
455
|
* populate it from, primary or otherwise (see the schema docstring).
|
|
459
456
|
*
|
|
460
|
-
* **Lazy `@mailwoman/filer/sdk` import
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
457
|
+
* **Lazy `@mailwoman/filer/sdk` import.** `readFRNFilingCandidates`/`pickPrimaryFRN` are loaded via `await
|
|
458
|
+
* import("@mailwoman/filer/sdk")`, memoized in `filerSDK` below, rather than a top-level static import — that barrel
|
|
459
|
+
* re-exports `cluster-filers.ts`, which pulls in `@mailwoman/match`/`record`/`registry`. A top-level import regressed
|
|
460
|
+
* `@mailwoman/bdc`'s import time ~32% for EVERY consumer, including ones that never populate providers at all; the
|
|
461
|
+
* dynamic import here only ever runs when a multi-FRN `provider_id` is actually encountered, so a `providers`-less
|
|
462
|
+
* build (or one whose providers are all single-FRN) pays nothing.
|
|
466
463
|
*/
|
|
467
464
|
async function populateBDCProviderTable(
|
|
468
465
|
kdb: DatabaseClient<BDCDatabase>,
|
|
@@ -625,7 +622,7 @@ export async function buildBDCDatabase(options: BuildBDCOptions): Promise<BuildB
|
|
|
625
622
|
// all survive the staging dedup as distinct staged rows. In `includeLocationIDs` mode that's correct: every BSL
|
|
626
623
|
// is a real, distinct row the caller asked to keep. In the default (NULL `location_id`) mode, when those BSLs
|
|
627
624
|
// ALSO share identical speeds/flags, they'd otherwise materialize as byte-identical rows, inflating `result.rows`
|
|
628
|
-
// and `layer_coverage.observed_rows` by the BSL count (
|
|
625
|
+
// and `layer_coverage.observed_rows` by the BSL count (~100x at real scale) — `SELECT DISTINCT`
|
|
629
626
|
// over every column EXCEPT `location_id` collapses those byte-identical BSL duplicates down to one row.
|
|
630
627
|
// IMPORTANT — this is NOT a guarantee of one row per (geoid, provider_id, technology_code) triple: BSLs at the
|
|
631
628
|
// same triple with DIFFERING speeds/flags are NOT the same tuple, so `SELECT DISTINCT` does not merge them —
|
|
@@ -661,8 +658,8 @@ export async function buildBDCDatabase(options: BuildBDCOptions): Promise<BuildB
|
|
|
661
658
|
// Coverage cell MUST be derived as the res-9 cell's H3 hierarchy parent — NOT a second,
|
|
662
659
|
// independent `latLngToCell(centroid, 6)` call. H3's cell hierarchy is not geometrically
|
|
663
660
|
// exact: a point's directly-indexed res-6 cell and its res-9 cell's `cellToParent(…, 6)`
|
|
664
|
-
// disagree for a real fraction of points (~6% empirically over CONUS
|
|
665
|
-
//
|
|
661
|
+
// disagree for a real fraction of points (~6% empirically over CONUS — hexagon/pentagon
|
|
662
|
+
// boundary artifacts). Deriving both `h3_cell` and the coverage cell from
|
|
666
663
|
// the SAME full res-9 index is what lets `filing-landscape.ts`'s reader reconstruct this
|
|
667
664
|
// exact coverage cell from nothing but the stored `h3_cell` (its `res9ShortCellToRes6Parent`
|
|
668
665
|
// applies `cellToParent` to the reconstructed res-9 cell) — builder and reader must derive
|
|
@@ -754,9 +751,9 @@ export async function buildBDCDatabase(options: BuildBDCOptions): Promise<BuildB
|
|
|
754
751
|
createdAt: new Date().toISOString(),
|
|
755
752
|
})
|
|
756
753
|
|
|
757
|
-
// bdc_provider population (2a decision 8 / 3a decision 6
|
|
758
|
-
// `options.providers`: when absent, this block never runs and `bdc_provider` stays empty
|
|
759
|
-
//
|
|
754
|
+
// bdc_provider population (2a decision 8 / 3a decision 6) — entirely additive and gated behind
|
|
755
|
+
// `options.providers`: when absent, this block never runs and `bdc_provider` stays empty (see
|
|
756
|
+
// `BuildBDCOptions.providers`'s docstring for the default-path guarantee).
|
|
760
757
|
let providersPopulated = 0
|
|
761
758
|
|
|
762
759
|
if (options.providers) {
|
package/sdk/common.ts
CHANGED
|
@@ -170,8 +170,8 @@ export interface RawBDCFile {
|
|
|
170
170
|
* 2-digit state or territory FIPS code.
|
|
171
171
|
*
|
|
172
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`.
|
|
174
|
-
*
|
|
173
|
+
* drops that dependency, same as `data-collection.ts`'s `FCCStateID`. Tighten it against `@mailwoman/tiger` if a
|
|
174
|
+
* downstream dictionary ever needs the literal union.
|
|
175
175
|
*
|
|
176
176
|
* Nullable in live data for rows not scoped to a specific state (e.g. Provider-category rows). Guarded in
|
|
177
177
|
* {@linkcode parseRawBDCFile} — a null value parses to an empty `stateCode` string.
|
|
@@ -180,7 +180,7 @@ export interface RawBDCFile {
|
|
|
180
180
|
/**
|
|
181
181
|
* State or territory name.
|
|
182
182
|
*
|
|
183
|
-
* Loosely typed as `string` — the Nexus original was `StateName` (via `@isp.nexus/tiger`). Same
|
|
183
|
+
* Loosely typed as `string` — the Nexus original was `StateName` (via `@isp.nexus/tiger`). Same deferral as
|
|
184
184
|
* `state_fips` above.
|
|
185
185
|
*/
|
|
186
186
|
state_name: string
|
|
@@ -234,8 +234,8 @@ export interface BDCFile {
|
|
|
234
234
|
/**
|
|
235
235
|
* The state or territory FIPS code.
|
|
236
236
|
*
|
|
237
|
-
* Loosely typed as `string` — see {@linkcode RawBDCFile} for the
|
|
238
|
-
* `
|
|
237
|
+
* Loosely typed as `string` — see {@linkcode RawBDCFile} for the deferral. Empty string when the raw `state_fips` was
|
|
238
|
+
* `null`.
|
|
239
239
|
*/
|
|
240
240
|
stateCode: string
|
|
241
241
|
/**
|
package/sdk/data-collection.ts
CHANGED
|
@@ -12,8 +12,8 @@ import type { BroadbandServicableLocationID } from "./location.ts"
|
|
|
12
12
|
* Snake_case identifier for a US State or Territory.
|
|
13
13
|
*
|
|
14
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
|
|
16
|
-
*
|
|
15
|
+
* port drops that dependency since nothing here consumes the literal union. Tighten it against `@mailwoman/tiger`'s
|
|
16
|
+
* `StateName` if a downstream dictionary ever needs it.
|
|
17
17
|
*
|
|
18
18
|
* @category FCC
|
|
19
19
|
* @title FCC State Identifier
|
package/sdk/download.ts
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Re-homed from Nexus's `sync/fcc/bdc/download-file.ts` (relicense-by-copy, no provenance headers),
|
|
8
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 —
|
|
10
|
-
* SQLite, not Parquet-backed (
|
|
9
|
+
* file with a row-count integrity check. All Parquet machinery is dropped here — `bdc.db` is
|
|
10
|
+
* SQLite, not Parquet-backed (`bdc/schema.ts`) — and the `.zip` itself isn't cached either; only the
|
|
11
11
|
* extracted CSV is written to `destinationDir`, and its presence alone is the cache check.
|
|
12
12
|
*
|
|
13
13
|
* The zip-extraction library also changes: the Nexus original's `extractSingleFileZip` used `adm-zip`
|
|
14
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
|
|
16
|
-
*
|
|
15
|
+
* `package.json` was checked, `tiger/` and `osm/` included — so `yauzl-promise` is a `bdc`-only
|
|
16
|
+
* dependency.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import * as fs from "node:fs/promises"
|
package/sdk/filing-landscape.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @license AGPL-3.0
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
5
|
*
|
|
6
|
-
* `filing_landscape` reader
|
|
6
|
+
* `filing_landscape` reader — the FOUR PRE-REGISTERED ACCEPTANCE GATES this whole phase
|
|
7
7
|
* is judged by. See `filing-landscape.test.ts` for the gate tests; this module is only the reader.
|
|
8
8
|
*
|
|
9
9
|
* Coverage check (the meaning-of-zero rule): a queried block counts as SURVEYED only when its res-6
|
|
@@ -18,31 +18,24 @@
|
|
|
18
18
|
* even for a cell with no filing rows of its own — a genuine "surveyed, zero providers here" result,
|
|
19
19
|
* the meaning-of-zero rule's POSITIVE case (covered but empty is not the same as never surveyed).
|
|
20
20
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* `cellToParent` rejects (`Cell arguments had incompatible resolutions`), verified empirically while
|
|
25
|
-
* building this reader. The correct reconstruction for a short cell captured at a KNOWN resolution R
|
|
26
|
-
* is a straight concatenation, not a shift: a full 64-bit H3 cell index is always
|
|
27
|
-
* `"8" + <resolution nibble> + <52 bits of base-cell + digit path, trailing padding included>`, and
|
|
28
|
-
* the 48-bit "short" form (`shortCellToInt`/`shortenH3Cell`) already carries exactly those low 52 bits
|
|
29
|
-
* verbatim — so `"8" + R.toString(16) + shortHex.padStart(13, "0")` reassembles the identical full
|
|
30
|
-
* index `latLngToCell` would have produced at resolution R. See {@link res9ShortCellToRes6Parent}.
|
|
21
|
+
* The res-6 parent is reconstructed from the STORED res-9 cell (`@mailwoman/spatial`'s `expandH3Cell`
|
|
22
|
+
* back to a full index, then `cellToParent`) rather than recomputed from the block centroid. See
|
|
23
|
+
* {@link res9ShortCellToRes6Parent}.
|
|
31
24
|
*
|
|
32
|
-
* This same formula is exactly what `build-bdc.ts` MUST use (and does
|
|
25
|
+
* This same formula is exactly what `build-bdc.ts` MUST use (and does) to derive the
|
|
33
26
|
* coverage cell it writes at build time — H3's cell hierarchy is not geometrically exact, so a
|
|
34
27
|
* `latLngToCell(centroid, 6)` computed independently of the stored res-9 cell disagrees with
|
|
35
28
|
* `cellToParent(res9Cell, 6)` for a real fraction of points (verified ~6% over CONUS). Builder and
|
|
36
|
-
* reader deriving the res-6 parent differently
|
|
37
|
-
* (real rows, real `layer_coverage` entry)
|
|
38
|
-
*
|
|
39
|
-
* coverage check (see the `surveyedUnits` accumulator below) precisely so that can't happen
|
|
29
|
+
* reader deriving the res-6 parent differently is a self-contradiction waiting to happen: a
|
|
30
|
+
* genuinely-surveyed block (real rows, real `layer_coverage` entry) reads back as
|
|
31
|
+
* `unknown_block_count` while its own rows still populate `filings`. `filings` is scoped to units that
|
|
32
|
+
* PASS the coverage check (see the `surveyedUnits` accumulator below) precisely so that can't happen: a
|
|
40
33
|
* block excluded from `surveyed_block_count` never contributes to `filings` either.
|
|
41
34
|
*/
|
|
42
35
|
|
|
43
36
|
import type { DatabaseClient } from "@mailwoman/core/kysley/client"
|
|
44
37
|
import { readLayerCoverage, readLayerManifest, type LayerContractDatabase } from "@mailwoman/core/layers"
|
|
45
|
-
import { shortCellToInt, type H3Cell } from "@mailwoman/spatial"
|
|
38
|
+
import { expandH3Cell, shortCellToInt, type H3Cell, type H3CellShort } from "@mailwoman/spatial"
|
|
46
39
|
import { cellToParent } from "h3-js"
|
|
47
40
|
import { sql, type Kysely } from "kysely"
|
|
48
41
|
|
|
@@ -155,13 +148,12 @@ function asContractDB(kdb: DatabaseClient<BDCDatabase>): Kysely<LayerContractDat
|
|
|
155
148
|
}
|
|
156
149
|
|
|
157
150
|
/**
|
|
158
|
-
* Reconstruct the res-6 ancestor of a res-9 short-cell int WITHOUT a centroid — see the module docstring for why
|
|
159
|
-
*
|
|
160
|
-
*
|
|
151
|
+
* Reconstruct the res-6 ancestor of a res-9 short-cell int WITHOUT a centroid — see the module docstring for why the
|
|
152
|
+
* centroid is the wrong input. Exported so tests can assert this agrees, cell-for-cell, with `build-bdc.ts`'s own
|
|
153
|
+
* coverage-cell derivation (the two MUST share this derivation — see that file's docstring).
|
|
161
154
|
*/
|
|
162
155
|
export function res9ShortCellToRes6Parent(h3CellShortInt: number): number {
|
|
163
|
-
const
|
|
164
|
-
const fullCell = `8${BDC_H3_RESOLUTION.toString(16)}${shortHex}` as H3Cell
|
|
156
|
+
const fullCell = expandH3Cell(h3CellShortInt.toString(16) as H3CellShort, BDC_H3_RESOLUTION)
|
|
165
157
|
const parentCell = cellToParent(fullCell, BDC_COVERAGE_H3_RESOLUTION) as H3Cell
|
|
166
158
|
|
|
167
159
|
return shortCellToInt(parentCell)
|
package/sdk/list-files.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Re-homed from Nexus's `sync/fcc/bdc/list-files.ts` (relicense-by-copy, no provenance headers):
|
|
8
8
|
* the `URLRoutePattern`-compiled route → a plain template literal (this port has no routing
|
|
9
9
|
* dependency), the `$BCDClient`-bound `retrieveAvailabilityFiles()` → a plain function taking a
|
|
10
|
-
* {@linkcode BDCClient}. Raw entries are parsed via
|
|
10
|
+
* {@linkcode BDCClient}. Raw entries are parsed via `parseRawBDCFile` and returned sorted
|
|
11
11
|
* ascending by revision (`compareRevisionAsc`) rather than in API order.
|
|
12
12
|
*/
|
|
13
13
|
|
|
@@ -4,15 +4,14 @@
|
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
5
|
*
|
|
6
6
|
* `nearestInfrastructure` — a coverage-paired k-nearest read over the telecom-infrastructure POI
|
|
7
|
-
* categories (`telecom_exchange`/`tower_comms
|
|
8
|
-
*
|
|
9
|
-
* table (
|
|
10
|
-
* real infrastructure to this claimed Broadband Serviceable Location, and does OUR layer even
|
|
11
|
-
* survey evidence for that area" in one call.
|
|
7
|
+
* categories (`telecom_exchange`/`tower_comms` — `@mailwoman/poi-taxonomy` categories, populated by the
|
|
8
|
+
* `--source osm` extractor) that a caller can join against ANY layer's own `layer_coverage`
|
|
9
|
+
* survey-completeness table (decision 7). Typical caller: a BDC filing scorer that wants "what's the
|
|
10
|
+
* nearest real infrastructure to this claimed Broadband Serviceable Location, and does OUR layer even
|
|
11
|
+
* have survey evidence for that area" in one call.
|
|
12
12
|
*
|
|
13
|
-
* Two
|
|
14
|
-
* `@mailwoman/core/layers` actually expose
|
|
15
|
-
* documents" instruction):
|
|
13
|
+
* Two shapes below are not the obvious ones, and both are forced by what `poi-lookup.ts` and
|
|
14
|
+
* `@mailwoman/core/layers` actually expose:
|
|
16
15
|
*
|
|
17
16
|
* - **`poiLookup` is an already-open {@link POILookup}, not a `POILookupOpts` this function
|
|
18
17
|
* constructs itself.** `POILookup`'s constructor eagerly loads the poi-taxonomy category dictionary
|
|
@@ -22,14 +21,14 @@
|
|
|
22
21
|
* `poi.db` ONCE and reuse the same `POILookup` — this wrapper takes that shape: the caller owns
|
|
23
22
|
* `POILookup`'s open/dispose lifecycle (`using poiLookup = new POILookup(...)`), we just call
|
|
24
23
|
* `.search()` on it.
|
|
25
|
-
* - **`nearestInfrastructure` is `async`, not
|
|
24
|
+
* - **`nearestInfrastructure` is `async`, not sync.** `readLayerCoverage`
|
|
26
25
|
* (`@mailwoman/core/layers`) is `Promise`-returning — every layer-contract read in this codebase is
|
|
27
26
|
* (`readLayerManifest`, `filingLandscape` itself) — so pairing each POI hit with its coverage cell
|
|
28
27
|
* means awaiting one `readLayerCoverage` call per hit. A sync signature can't await that.
|
|
29
28
|
*
|
|
30
29
|
* {@link res9ShortCellToRes6Parent} (exported by `filing-landscape.ts`; see that file's docstring for
|
|
31
|
-
* why the res-
|
|
32
|
-
*
|
|
30
|
+
* why the res-6 parent is reconstructed from the STORED res-9 cell rather than recomputed from the
|
|
31
|
+
* centroid) turns each hit's res-9 cell into the res-6 cell every layer in this repo aggregates
|
|
33
32
|
* coverage at (poi.db's own convention; `bdc.db`'s `BDC_COVERAGE_H3_RESOLUTION` matches it
|
|
34
33
|
* deliberately — see `schema.ts`). `POI_H3_RESOLUTION` (`poi-lookup.ts`) is ALSO 9, so the two layers'
|
|
35
34
|
* spines agree without this module hardcoding a resolution of its own.
|
package/sdk/plausibility.ts
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
* @license AGPL-3.0
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
5
|
*
|
|
6
|
-
* `plausibilityCheck` (
|
|
7
|
-
* `filingLandscape` (
|
|
8
|
-
* broadband-service claim, under the registry-backed doctrine's positive-evidence-only
|
|
9
|
-
* §4): a BDC filing or a nearby infrastructure hit can RAISE confidence; their absence
|
|
10
|
-
* read as "unknown" or "no supporting evidence found, coverage permitting" — NEVER as
|
|
11
|
-
* The four §7-2b acceptance gates
|
|
12
|
-
* them but doesn't assert them itself.
|
|
6
|
+
* `plausibilityCheck` (spec §3.2/§4) — the heart of the BDC plausibility vertical. Composes
|
|
7
|
+
* `filingLandscape` (`bdc/sdk/filing-landscape.ts`) + `nearestInfrastructure` into ONE evidence bundle
|
|
8
|
+
* over a single broadband-service claim, under the registry-backed doctrine's positive-evidence-only
|
|
9
|
+
* invariant (spec §4): a BDC filing or a nearby infrastructure hit can RAISE confidence; their absence
|
|
10
|
+
* can only ever read as "unknown" or "no supporting evidence found, coverage permitting" — NEVER as
|
|
11
|
+
* "implausible." The four §7-2b acceptance gates are asserted in `plausibility.test.ts`'s
|
|
12
|
+
* `describe("§7-2b gates")` block; this module is designed for them but doesn't assert them itself.
|
|
13
13
|
*
|
|
14
14
|
* **Claim resolution (decision 4).** A claim's spatial key resolves two INDEPENDENT ways:
|
|
15
15
|
*
|
|
@@ -88,25 +88,26 @@
|
|
|
88
88
|
* filing coverage alone — it degrades to filing-covered → `"low"`, filing-unknown/absent →
|
|
89
89
|
* `"insufficient_survey_data"`. Rationale: spec §4 frames physical co-presence as the vertical's
|
|
90
90
|
* distinguishing "falsifier that only fires positively" and reserves `"high"` for genuine two-channel
|
|
91
|
-
* corroboration (Gate 2
|
|
91
|
+
* corroboration (§7-2b Gate 2: "matching filing + nearby plant in covered cells… high"); a tech
|
|
92
92
|
* that structurally can never offer a second channel should not be able to claim the same top
|
|
93
93
|
* confidence a fiber claim earns by actually having one available. This reading is more conservative
|
|
94
|
-
* than the alternative (treating the missing axis as inert and reporting `"high"` off filing alone)
|
|
95
|
-
*
|
|
94
|
+
* than the alternative (treating the missing axis as inert and reporting `"high"` off filing alone),
|
|
95
|
+
* and nothing pins it: none of the four §7-2b gates exercises a no-physical-falsifier tech code.
|
|
96
96
|
*
|
|
97
|
-
* **
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
97
|
+
* **The per-layer coverage-resolution sanity check.** Neither bdc.db's nor poi.db's `layer_manifest`
|
|
98
|
+
* records the COVERAGE-cell h3 resolution (6) that `res9ShortCellToRes6Parent` hardcodes on both sides
|
|
99
|
+
* — only each layer's ROW-spine resolution (9, `spineKeys.h3.resolution`) is ever recorded. Closing
|
|
100
|
+
* that gap properly needs an addition to the layer contract itself (`@mailwoman/core/layers`), which
|
|
101
|
+
* does not exist yet. What IS practical and
|
|
102
102
|
* cheap: each manifest is a single-row table already read at most once per call here, so whenever a layer is
|
|
103
103
|
* WIRED — `bdcDB`, `poi`, or both, checked independently — {@link assertLayerSpineResolution} compares that one
|
|
104
104
|
* layer's recorded `spineKeys.h3.resolution` directly against the `BDC_H3_RESOLUTION` constant `pointCell` is
|
|
105
105
|
* actually derived from, and throws on a mismatch, catching a layer built at a different spine resolution before it
|
|
106
106
|
* silently mis-joins a coverage cell. This is TWO-SIDED, not gated on both layers being present together: a
|
|
107
107
|
* poi-only call still checks poi's own recorded resolution, since `readLayerCoverage`'s poi-side join key (below)
|
|
108
|
-
* is derived from `BDC_H3_RESOLUTION` regardless of whether `bdcDB` is wired at all
|
|
109
|
-
*
|
|
108
|
+
* is derived from `BDC_H3_RESOLUTION` regardless of whether `bdcDB` is wired at all — comparing each manifest
|
|
109
|
+
* against the constant, rather than the two manifests against each other, is what makes a single-layer call
|
|
110
|
+
* checkable at all. It
|
|
110
111
|
* can NOT catch a layer whose row spine is 9 but whose COVERAGE cells were derived at some OTHER resolution than 6
|
|
111
112
|
* — that gap needs the schema addition, not a runtime assertion.
|
|
112
113
|
*/
|
|
@@ -169,12 +170,12 @@ export type PlausibilityEvidence =
|
|
|
169
170
|
| { type: "abstain"; reason: PlausibilityAbstainReason; layer?: string }
|
|
170
171
|
|
|
171
172
|
/**
|
|
172
|
-
* One evidence channel's survey-completeness state for THIS claim, WITH the reason a non-`"covered"` state applies
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
173
|
+
* One evidence channel's survey-completeness state for THIS claim, WITH the reason a non-`"covered"` state applies.
|
|
174
|
+
* `coverage_confidence` alone folds several genuinely different situations into the same
|
|
175
|
+
* `"low"`/`"insufficient_survey_data"` verdict (a tech with no physical falsifier at all vs. a real poi survey gap vs.
|
|
176
|
+
* a geoid-only claim with no coordinate to search from); this axis state is what tells them apart. `"not_applicable"`
|
|
177
|
+
* and `"no_coordinate"` are only ever produced for the physical axis; the filing axis only ever reaches `"covered"`,
|
|
178
|
+
* `"layer_missing"`, or `"cell_unsurveyed"`.
|
|
178
179
|
*/
|
|
179
180
|
export type PlausibilityCoverageAxisState =
|
|
180
181
|
| "covered"
|
|
@@ -203,7 +204,7 @@ export type PlausibilityCoverageAxisState =
|
|
|
203
204
|
|
|
204
205
|
/**
|
|
205
206
|
* Per-axis attribution for {@link PlausibilityBundle.coverage_confidence} — see {@link PlausibilityCoverageAxisState}.
|
|
206
|
-
*
|
|
207
|
+
* Reported alongside `coverage_confidence`, never in place of it: the coarse field stays the stable public surface.
|
|
207
208
|
*/
|
|
208
209
|
export interface PlausibilityCoverageDetail {
|
|
209
210
|
filing: PlausibilityCoverageAxisState
|
|
@@ -215,8 +216,8 @@ export interface PlausibilityBundle {
|
|
|
215
216
|
evidence_found: PlausibilityEvidence[]
|
|
216
217
|
coverage_confidence: "high" | "low" | "insufficient_survey_data"
|
|
217
218
|
/**
|
|
218
|
-
* Per-axis WHY behind `coverage_confidence
|
|
219
|
-
*
|
|
219
|
+
* Per-axis WHY behind `coverage_confidence`. ALWAYS present, mirroring `block_resolution`'s always-present
|
|
220
|
+
* discipline.
|
|
220
221
|
*/
|
|
221
222
|
coverage_detail: PlausibilityCoverageDetail
|
|
222
223
|
/**
|
|
@@ -245,7 +246,7 @@ export interface GeocodeLike {
|
|
|
245
246
|
}
|
|
246
247
|
|
|
247
248
|
/**
|
|
248
|
-
* The already-open infra layer this scorer composes against
|
|
249
|
+
* The already-open infra layer this scorer composes against {@link nearestInfrastructure}. The caller owns BOTH
|
|
249
250
|
* handles' open/dispose lifecycle (mirrors `nearestInfrastructure`'s own `using poiLookup = new POILookup(...)`
|
|
250
251
|
* precedent). `contractDB` is used two ways: passed straight through to `nearestInfrastructure` (per-hit coverage), and
|
|
251
252
|
* read directly here (the whole-cell coverage check this module needs for `coverage_confidence`, independent of whether
|
|
@@ -263,10 +264,9 @@ export interface PlausibilityDeps {
|
|
|
263
264
|
}
|
|
264
265
|
|
|
265
266
|
/**
|
|
266
|
-
* Tech → physical-plant category mapping
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
* {@link physicalCategoriesForTechnology}).
|
|
267
|
+
* Tech → physical-plant category mapping. Fiber implies the three infrastructure-extension categories a fiber network
|
|
268
|
+
* plausibly touches; the three fixed-wireless codes imply a comms tower; every other code maps to `[]` — no physical
|
|
269
|
+
* falsifier is claimed for it (see {@link physicalCategoriesForTechnology}).
|
|
270
270
|
*/
|
|
271
271
|
export const PLAUSIBILITY_TECH_PHYSICAL_CATEGORIES: Readonly<Record<number, readonly string[]>> = {
|
|
272
272
|
[BroadbandTechnologyCode.OpticalCarrierFiber]: ["telecom_exchange", "telecom_cabinet", "data_center"],
|
|
@@ -359,18 +359,17 @@ function combineCoverage(
|
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
/**
|
|
362
|
-
* See the module docstring's
|
|
362
|
+
* See the module docstring's coverage-resolution note. Throws when a WIRED layer's manifest disagrees with
|
|
363
363
|
* `BDC_H3_RESOLUTION` — the single constant `plausibilityCheck` actually uses at runtime to derive both the
|
|
364
364
|
* filing-lookup cell (bdc side, via `pointCell`) and the coverage-cell join key `readLayerCoverage` is read against
|
|
365
365
|
* (poi side, via `res9ShortCellToRes6Parent(pointCell)`).
|
|
366
366
|
*
|
|
367
|
-
*
|
|
368
|
-
* only
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
* entirely, not just a disagreement between two present layers.
|
|
367
|
+
* Checked independently PER LAYER, whenever THAT layer is wired — not only when `bdcDB` and `poi` are wired together. A
|
|
368
|
+
* poi-only call still needs poi's own recorded resolution checked, because `pointCell` is computed unconditionally from
|
|
369
|
+
* `BDC_H3_RESOLUTION` and still drives the poi coverage-cell read below. Comparing each layer directly against the
|
|
370
|
+
* constant, rather than the two manifests against each other, is also strictly stronger: it catches a layer built under
|
|
371
|
+
* a since-changed `BDC_H3_RESOLUTION` even when the OTHER layer is absent entirely, not just a disagreement between two
|
|
372
|
+
* present layers.
|
|
374
373
|
*/
|
|
375
374
|
async function assertLayerSpineResolution(
|
|
376
375
|
layer: "bdc" | "poi",
|
|
@@ -427,9 +426,9 @@ export async function plausibilityCheck(claim: PlausibilityClaim, deps: Plausibi
|
|
|
427
426
|
? shortCellToInt(latLngToCell(point.coordinates[1], point.coordinates[0], BDC_H3_RESOLUTION) as H3Cell)
|
|
428
427
|
: undefined
|
|
429
428
|
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
429
|
+
// Cheap, one-time per-layer sanity check — see the module docstring's coverage-resolution note. Runs
|
|
430
|
+
// independently per WIRED layer, not only when both are present: a poi-only call still joins poi's coverage
|
|
431
|
+
// table against a BDC_H3_RESOLUTION-derived cell (below) and must not do so unchecked.
|
|
433
432
|
if (deps.bdcDB) {
|
|
434
433
|
await assertLayerSpineResolution("bdc", asContractDB(deps.bdcDB), BDC_H3_RESOLUTION)
|
|
435
434
|
}
|
|
@@ -482,9 +481,8 @@ export async function plausibilityCheck(claim: PlausibilityClaim, deps: Plausibi
|
|
|
482
481
|
} else if (!point) {
|
|
483
482
|
// Geoid-only claim, no coordinate resolvable — see the module docstring's claim-resolution note. A real
|
|
484
483
|
// capability gap, not a missing-layer abstain: no evidence entry is fabricated, but the axis still
|
|
485
|
-
// degrades honestly for coverage_confidence
|
|
486
|
-
//
|
|
487
|
-
// finding 1).
|
|
484
|
+
// degrades honestly for coverage_confidence, naming ITS OWN reason in `coverage_detail` rather than
|
|
485
|
+
// folding into the same generic "unknown" as `"layer_missing"`.
|
|
488
486
|
physicalCoverage = "no_coordinate"
|
|
489
487
|
} else {
|
|
490
488
|
const hits = await nearestInfrastructure(deps.poi.lookup, deps.poi.contractDB, {
|