@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.
- package/README.md +7 -0
- package/index.ts +2 -0
- package/out/index.d.ts +3 -0
- package/out/index.d.ts.map +1 -0
- package/out/index.js +3 -0
- package/out/index.js.map +1 -0
- package/out/schema.d.ts +138 -0
- package/out/schema.d.ts.map +1 -0
- package/out/schema.js +88 -0
- package/out/schema.js.map +1 -0
- package/out/sdk/build-bdc.d.ts +214 -0
- package/out/sdk/build-bdc.d.ts.map +1 -0
- package/out/sdk/build-bdc.js +500 -0
- package/out/sdk/build-bdc.js.map +1 -0
- package/out/sdk/buildings.d.ts +84 -0
- package/out/sdk/buildings.d.ts.map +1 -0
- package/out/sdk/buildings.js +75 -0
- package/out/sdk/buildings.js.map +1 -0
- package/out/sdk/client.d.ts +237 -0
- package/out/sdk/client.d.ts.map +1 -0
- package/out/sdk/client.js +430 -0
- package/out/sdk/client.js.map +1 -0
- package/out/sdk/common.d.ts +279 -0
- package/out/sdk/common.d.ts.map +1 -0
- package/out/sdk/common.js +162 -0
- package/out/sdk/common.js.map +1 -0
- package/out/sdk/data-collection.d.ts +244 -0
- package/out/sdk/data-collection.d.ts.map +1 -0
- package/out/sdk/data-collection.js +50 -0
- package/out/sdk/data-collection.js.map +1 -0
- package/out/sdk/download.d.ts +36 -0
- package/out/sdk/download.d.ts.map +1 -0
- package/out/sdk/download.js +76 -0
- package/out/sdk/download.js.map +1 -0
- package/out/sdk/filing-dates.d.ts +57 -0
- package/out/sdk/filing-dates.d.ts.map +1 -0
- package/out/sdk/filing-dates.js +50 -0
- package/out/sdk/filing-dates.js.map +1 -0
- package/out/sdk/filing-landscape.d.ts +124 -0
- package/out/sdk/filing-landscape.d.ts.map +1 -0
- package/out/sdk/filing-landscape.js +216 -0
- package/out/sdk/filing-landscape.js.map +1 -0
- package/out/sdk/index.d.ts +15 -0
- package/out/sdk/index.d.ts.map +1 -0
- package/out/sdk/index.js +15 -0
- package/out/sdk/index.js.map +1 -0
- package/out/sdk/list-files.d.ts +39 -0
- package/out/sdk/list-files.d.ts.map +1 -0
- package/out/sdk/list-files.js +26 -0
- package/out/sdk/list-files.js.map +1 -0
- package/out/sdk/location.d.ts +40 -0
- package/out/sdk/location.d.ts.map +1 -0
- package/out/sdk/location.js +23 -0
- package/out/sdk/location.js.map +1 -0
- package/out/sdk/nearest-infrastructure.d.ts +84 -0
- package/out/sdk/nearest-infrastructure.d.ts.map +1 -0
- package/out/sdk/nearest-infrastructure.js +84 -0
- package/out/sdk/nearest-infrastructure.js.map +1 -0
- package/out/sdk/parsing.d.ts +57 -0
- package/out/sdk/parsing.d.ts.map +1 -0
- package/out/sdk/parsing.js +87 -0
- package/out/sdk/parsing.js.map +1 -0
- package/out/sdk/plausibility.d.ts +256 -0
- package/out/sdk/plausibility.d.ts.map +1 -0
- package/out/sdk/plausibility.js +334 -0
- package/out/sdk/plausibility.js.map +1 -0
- package/out/sdk/technologies.d.ts +229 -0
- package/out/sdk/technologies.d.ts.map +1 -0
- package/out/sdk/technologies.js +217 -0
- package/out/sdk/technologies.js.map +1 -0
- package/package.json +97 -0
- package/schema.ts +172 -0
- package/sdk/build-bdc.ts +810 -0
- package/sdk/buildings.ts +87 -0
- package/sdk/client.ts +643 -0
- package/sdk/common.ts +363 -0
- package/sdk/data-collection.ts +268 -0
- package/sdk/download.ts +91 -0
- package/sdk/filing-dates.ts +88 -0
- package/sdk/filing-landscape.ts +287 -0
- package/sdk/index.ts +14 -0
- package/sdk/list-files.ts +72 -0
- package/sdk/location.ts +45 -0
- package/sdk/nearest-infrastructure.ts +129 -0
- package/sdk/parsing.ts +124 -0
- package/sdk/plausibility.ts +512 -0
- package/sdk/technologies.ts +258 -0
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
* Code indicating confidence in the association between the textual address and the location.
|
|
9
|
+
*
|
|
10
|
+
* @category NTIA
|
|
11
|
+
* @category FCC
|
|
12
|
+
*/
|
|
13
|
+
export const AddressConfidenceCode = {
|
|
14
|
+
High: "1",
|
|
15
|
+
Medium: "2",
|
|
16
|
+
Low: "3",
|
|
17
|
+
SuccessfulChallenge: "4",
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* A modeled land use designation estimated from assembled county assessor information.
|
|
21
|
+
*
|
|
22
|
+
* @category NTIA
|
|
23
|
+
* @category FCC
|
|
24
|
+
*/
|
|
25
|
+
export const LandUseCode = {
|
|
26
|
+
Residential: 1,
|
|
27
|
+
Land: 2,
|
|
28
|
+
Business: 3,
|
|
29
|
+
Unknown: 4,
|
|
30
|
+
Agriculture: 5,
|
|
31
|
+
Community: 6,
|
|
32
|
+
Industrial: 7,
|
|
33
|
+
Recreation: 8,
|
|
34
|
+
Utility: 9,
|
|
35
|
+
MixedUse: 10,
|
|
36
|
+
Transportation: 11,
|
|
37
|
+
Water: 12,
|
|
38
|
+
Communications: 13,
|
|
39
|
+
Row: 14,
|
|
40
|
+
Wireless: 15,
|
|
41
|
+
Other: 0,
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Flag indicating if the location is a broadband serviceable location.
|
|
45
|
+
*/
|
|
46
|
+
export const BSLFlag = {
|
|
47
|
+
Serviceable: 1,
|
|
48
|
+
NotServiceable: 0,
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=data-collection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-collection.js","sourceRoot":"","sources":["../../sdk/data-collection.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAiBH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACpC,IAAI,EAAE,GAAG;IACT,MAAM,EAAE,GAAG;IACX,GAAG,EAAE,GAAG;IACR,mBAAmB,EAAE,GAAG;CACf,CAAA;AAQV;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG;IAC1B,WAAW,EAAE,CAAC;IACd,IAAI,EAAE,CAAC;IACP,QAAQ,EAAE,CAAC;IACX,OAAO,EAAE,CAAC;IACV,WAAW,EAAE,CAAC;IACd,SAAS,EAAE,CAAC;IACZ,UAAU,EAAE,CAAC;IACb,UAAU,EAAE,CAAC;IACb,OAAO,EAAE,CAAC;IACV,QAAQ,EAAE,EAAE;IACZ,cAAc,EAAE,EAAE;IAClB,KAAK,EAAE,EAAE;IACT,cAAc,EAAE,EAAE;IAClB,GAAG,EAAE,EAAE;IACP,QAAQ,EAAE,EAAE;IACZ,KAAK,EAAE,CAAC;CACC,CAAA;AAQV;;GAEG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG;IACtB,WAAW,EAAE,CAAC;IACd,cAAc,EAAE,CAAC;CACR,CAAA"}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
import type { BDCClient } from "./client.ts";
|
|
19
|
+
import { type BDCFile } from "./common.ts";
|
|
20
|
+
/**
|
|
21
|
+
* Download and cache an FCC BDC availability file, extracting its zip-wrapped CSV to `destinationDir`.
|
|
22
|
+
*
|
|
23
|
+
* Cache-if-exists: if the extracted CSV already exists at the destination path, this returns immediately without
|
|
24
|
+
* issuing any network request. Otherwise it downloads the zip via `client`, extracts the CSV, writes it to
|
|
25
|
+
* `destinationDir`, and returns the written path. Only the extracted CSV is ever cached — the intermediate `.zip` is
|
|
26
|
+
* never written to disk.
|
|
27
|
+
*
|
|
28
|
+
* THIS FILE OWNS THE CACHE FOR THE DOWNLOAD PATH, which is why `BDCClient.getArrayBuffer` switches the client's own
|
|
29
|
+
* response cache off: the `existsSync`-equivalent check above is the real cache hit, and running a
|
|
30
|
+
* multi-hundred-megabyte archive through a JSON-validating disk cache would write a second, unreadable copy of a file
|
|
31
|
+
* already on disk here.
|
|
32
|
+
*
|
|
33
|
+
* @returns The path of the extracted (and now cached) CSV file.
|
|
34
|
+
*/
|
|
35
|
+
export declare function downloadBDCFile(client: BDCClient, file: BDCFile, destinationDir: string): Promise<string>;
|
|
36
|
+
//# sourceMappingURL=download.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../sdk/download.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAOH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,EAAqB,KAAK,OAAO,EAAE,MAAM,aAAa,CAAA;AA+B7D;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAoB/G"}
|
|
@@ -0,0 +1,76 @@
|
|
|
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
|
+
import * as fs from "node:fs/promises";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import { fromBuffer } from "yauzl-promise";
|
|
21
|
+
import { BDCFilingDataType } from "./common.js";
|
|
22
|
+
/**
|
|
23
|
+
* Extract the first file entry of a zip archive buffer into a single in-memory `Buffer`.
|
|
24
|
+
*
|
|
25
|
+
* BDC availability downloads are always a single zip-wrapped CSV, so — like the Nexus original — this doesn't walk
|
|
26
|
+
* every entry, just the first non-directory one.
|
|
27
|
+
*/
|
|
28
|
+
async function extractSingleFileZip(zippedBuffer) {
|
|
29
|
+
const zip = await fromBuffer(zippedBuffer);
|
|
30
|
+
try {
|
|
31
|
+
for await (const entry of zip) {
|
|
32
|
+
if (entry.filename.endsWith("/"))
|
|
33
|
+
continue;
|
|
34
|
+
const readStream = await entry.openReadStream();
|
|
35
|
+
const chunks = [];
|
|
36
|
+
for await (const chunk of readStream) {
|
|
37
|
+
chunks.push(chunk);
|
|
38
|
+
}
|
|
39
|
+
return Buffer.concat(chunks);
|
|
40
|
+
}
|
|
41
|
+
throw new Error("extractSingleFileZip: no file entries found in zip archive.");
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await zip.close();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Download and cache an FCC BDC availability file, extracting its zip-wrapped CSV to `destinationDir`.
|
|
49
|
+
*
|
|
50
|
+
* Cache-if-exists: if the extracted CSV already exists at the destination path, this returns immediately without
|
|
51
|
+
* issuing any network request. Otherwise it downloads the zip via `client`, extracts the CSV, writes it to
|
|
52
|
+
* `destinationDir`, and returns the written path. Only the extracted CSV is ever cached — the intermediate `.zip` is
|
|
53
|
+
* never written to disk.
|
|
54
|
+
*
|
|
55
|
+
* THIS FILE OWNS THE CACHE FOR THE DOWNLOAD PATH, which is why `BDCClient.getArrayBuffer` switches the client's own
|
|
56
|
+
* response cache off: the `existsSync`-equivalent check above is the real cache hit, and running a
|
|
57
|
+
* multi-hundred-megabyte archive through a JSON-validating disk cache would write a second, unreadable copy of a file
|
|
58
|
+
* already on disk here.
|
|
59
|
+
*
|
|
60
|
+
* @returns The path of the extracted (and now cached) CSV file.
|
|
61
|
+
*/
|
|
62
|
+
export async function downloadBDCFile(client, file, destinationDir) {
|
|
63
|
+
const csvPath = path.join(destinationDir, `${file.fileName}.csv`);
|
|
64
|
+
const alreadyCached = await fs
|
|
65
|
+
.access(csvPath)
|
|
66
|
+
.then(() => true)
|
|
67
|
+
.catch(() => false);
|
|
68
|
+
if (alreadyCached)
|
|
69
|
+
return csvPath;
|
|
70
|
+
const zippedArrayBuffer = await client.getArrayBuffer(`/map/downloads/downloadFile/${BDCFilingDataType.Availability}/${file.fileID}`);
|
|
71
|
+
const csvBuffer = await extractSingleFileZip(Buffer.from(zippedArrayBuffer));
|
|
72
|
+
await fs.mkdir(destinationDir, { recursive: true });
|
|
73
|
+
await fs.writeFile(csvPath, csvBuffer);
|
|
74
|
+
return csvPath;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=download.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"download.js","sourceRoot":"","sources":["../../sdk/download.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAA;AACtC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AAEjC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAG1C,OAAO,EAAE,iBAAiB,EAAgB,MAAM,aAAa,CAAA;AAE7D;;;;;GAKG;AACH,KAAK,UAAU,oBAAoB,CAAC,YAAoB;IACvD,MAAM,GAAG,GAAG,MAAM,UAAU,CAAC,YAAY,CAAC,CAAA;IAE1C,IAAI,CAAC;QACJ,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,EAAE,CAAC;YAC/B,IAAI,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC;gBAAE,SAAQ;YAE1C,MAAM,UAAU,GAAG,MAAM,KAAK,CAAC,cAAc,EAAE,CAAA;YAC/C,MAAM,MAAM,GAAa,EAAE,CAAA;YAE3B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,UAAU,EAAE,CAAC;gBACtC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;YACnB,CAAC;YAED,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QAC7B,CAAC;QAED,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAA;IAC/E,CAAC;YAAS,CAAC;QACV,MAAM,GAAG,CAAC,KAAK,EAAE,CAAA;IAClB,CAAC;AACF,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,MAAiB,EAAE,IAAa,EAAE,cAAsB;IAC7F,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,GAAG,IAAI,CAAC,QAAQ,MAAM,CAAC,CAAA;IAEjE,MAAM,aAAa,GAAG,MAAM,EAAE;SAC5B,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC;SAChB,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAA;IAEpB,IAAI,aAAa;QAAE,OAAO,OAAO,CAAA;IAEjC,MAAM,iBAAiB,GAAG,MAAM,MAAM,CAAC,cAAc,CACpD,+BAA+B,iBAAiB,CAAC,YAAY,IAAI,IAAI,CAAC,MAAM,EAAE,CAC9E,CAAA;IAED,MAAM,SAAS,GAAG,MAAM,oBAAoB,CAAC,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAA;IAE5E,MAAM,EAAE,CAAC,KAAK,CAAC,cAAc,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACnD,MAAM,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;IAEtC,OAAO,OAAO,CAAA;AACf,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software.
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
* @file FCC BDC filing-date discovery + vintage resolution.
|
|
6
|
+
*
|
|
7
|
+
* Re-homed from Nexus's `sync/fcc/bdc/filing-dates.ts` (relicense-by-copy, no provenance
|
|
8
|
+
* headers): the `$BCDClient`-bound `retrieveFilingDates()` → a plain function taking a
|
|
9
|
+
* {@linkcode BDCClient}.
|
|
10
|
+
*
|
|
11
|
+
* CACHING MOVED TO THE CLIENT. Both the Nexus original (one `<filingType>-dates.json` per filing
|
|
12
|
+
* type) and this port's first cut (`dataRootPath("bdc", "cache", "filing-dates.json")`, unfiltered)
|
|
13
|
+
* hand-rolled a JSON file cache here. `BDCClient` is built on `APIClient` and now carries an on-disk
|
|
14
|
+
* response cache of its own, so the hand-rolled one was the exact duplication that migration exists to
|
|
15
|
+
* remove — and it was worse than what replaced it: it had NO expiry, so a machine that resolved a
|
|
16
|
+
* vintage once would never see the next one FCC published without an explicit `skipCache`. The
|
|
17
|
+
* client's cache has a TTL chosen against the filing cadence, and `skipCache` now maps onto a
|
|
18
|
+
* per-request cache bypass with the same meaning it always had.
|
|
19
|
+
*/
|
|
20
|
+
import type { BDCClient } from "./client.ts";
|
|
21
|
+
import type { BDCFilingDataType } from "./common.ts";
|
|
22
|
+
/**
|
|
23
|
+
* One entry from the FCC BDC's `/map/listAsOfDates` endpoint.
|
|
24
|
+
*/
|
|
25
|
+
export interface FCCAsOfDateEntry {
|
|
26
|
+
data_type: BDCFilingDataType;
|
|
27
|
+
/**
|
|
28
|
+
* @format date
|
|
29
|
+
*/
|
|
30
|
+
as_of_date: string;
|
|
31
|
+
}
|
|
32
|
+
export interface RetrieveFilingDatesParams {
|
|
33
|
+
/**
|
|
34
|
+
* Which filing type's dates to return — the full cached/fetched set is filtered down to this type.
|
|
35
|
+
*/
|
|
36
|
+
filingType: BDCFilingDataType;
|
|
37
|
+
/**
|
|
38
|
+
* Bypass the client's response cache and always fetch fresh from the API. Defaults to `false`.
|
|
39
|
+
*/
|
|
40
|
+
skipCache?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Retrieve the FCC BDC's available filing `as_of_date`s for a given filing type.
|
|
44
|
+
*
|
|
45
|
+
* One `listAsOfDates` call answers every filing type — the full unfiltered response is what the client caches, and this
|
|
46
|
+
* filters it down on read — so asking for a second filing type inside the TTL costs no request at all. At ten requests
|
|
47
|
+
* per minute that is worth six seconds each time.
|
|
48
|
+
*/
|
|
49
|
+
export declare function retrieveFilingDates(client: BDCClient, { filingType, skipCache }: RetrieveFilingDatesParams): Promise<FCCAsOfDateEntry[]>;
|
|
50
|
+
/**
|
|
51
|
+
* Pick the latest (most recent) `as_of_date` among `entries` for the given `dataType`.
|
|
52
|
+
*
|
|
53
|
+
* Comparison is by parsed `Date` value, not string ordering — the FCC's `as_of_date` values are `date`-formatted
|
|
54
|
+
* (`YYYY-MM-DD`), which happens to sort correctly as strings too, but comparing as dates is the honest contract.
|
|
55
|
+
*/
|
|
56
|
+
export declare function resolveLatestVintage(entries: readonly FCCAsOfDateEntry[], dataType: BDCFilingDataType): string;
|
|
57
|
+
//# sourceMappingURL=filing-dates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filing-dates.d.ts","sourceRoot":"","sources":["../../sdk/filing-dates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAEpD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC,SAAS,EAAE,iBAAiB,CAAA;IAC5B;;OAEG;IACH,UAAU,EAAE,MAAM,CAAA;CAClB;AAMD,MAAM,WAAW,yBAAyB;IACzC;;OAEG;IACH,UAAU,EAAE,iBAAiB,CAAA;IAC7B;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACxC,MAAM,EAAE,SAAS,EACjB,EAAE,UAAU,EAAE,SAAiB,EAAE,EAAE,yBAAyB,GAC1D,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAI7B;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,EAAE,QAAQ,EAAE,iBAAiB,GAAG,MAAM,CAgB9G"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software.
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
* @file FCC BDC filing-date discovery + vintage resolution.
|
|
6
|
+
*
|
|
7
|
+
* Re-homed from Nexus's `sync/fcc/bdc/filing-dates.ts` (relicense-by-copy, no provenance
|
|
8
|
+
* headers): the `$BCDClient`-bound `retrieveFilingDates()` → a plain function taking a
|
|
9
|
+
* {@linkcode BDCClient}.
|
|
10
|
+
*
|
|
11
|
+
* CACHING MOVED TO THE CLIENT. Both the Nexus original (one `<filingType>-dates.json` per filing
|
|
12
|
+
* type) and this port's first cut (`dataRootPath("bdc", "cache", "filing-dates.json")`, unfiltered)
|
|
13
|
+
* hand-rolled a JSON file cache here. `BDCClient` is built on `APIClient` and now carries an on-disk
|
|
14
|
+
* response cache of its own, so the hand-rolled one was the exact duplication that migration exists to
|
|
15
|
+
* remove — and it was worse than what replaced it: it had NO expiry, so a machine that resolved a
|
|
16
|
+
* vintage once would never see the next one FCC published without an explicit `skipCache`. The
|
|
17
|
+
* client's cache has a TTL chosen against the filing cadence, and `skipCache` now maps onto a
|
|
18
|
+
* per-request cache bypass with the same meaning it always had.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Retrieve the FCC BDC's available filing `as_of_date`s for a given filing type.
|
|
22
|
+
*
|
|
23
|
+
* One `listAsOfDates` call answers every filing type — the full unfiltered response is what the client caches, and this
|
|
24
|
+
* filters it down on read — so asking for a second filing type inside the TTL costs no request at all. At ten requests
|
|
25
|
+
* per minute that is worth six seconds each time.
|
|
26
|
+
*/
|
|
27
|
+
export async function retrieveFilingDates(client, { filingType, skipCache = false }) {
|
|
28
|
+
const body = await client.get("/map/listAsOfDates", undefined, { skipCache });
|
|
29
|
+
return body.data.filter((entry) => entry.data_type === filingType);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Pick the latest (most recent) `as_of_date` among `entries` for the given `dataType`.
|
|
33
|
+
*
|
|
34
|
+
* Comparison is by parsed `Date` value, not string ordering — the FCC's `as_of_date` values are `date`-formatted
|
|
35
|
+
* (`YYYY-MM-DD`), which happens to sort correctly as strings too, but comparing as dates is the honest contract.
|
|
36
|
+
*/
|
|
37
|
+
export function resolveLatestVintage(entries, dataType) {
|
|
38
|
+
const matching = entries.filter((entry) => entry.data_type === dataType);
|
|
39
|
+
if (!matching.length) {
|
|
40
|
+
throw new Error(`resolveLatestVintage: no filing-date entries found for data_type "${dataType}"`);
|
|
41
|
+
}
|
|
42
|
+
let latest = matching[0];
|
|
43
|
+
for (const entry of matching) {
|
|
44
|
+
if (new Date(entry.as_of_date).getTime() > new Date(latest.as_of_date).getTime()) {
|
|
45
|
+
latest = entry;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return latest.as_of_date;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=filing-dates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filing-dates.js","sourceRoot":"","sources":["../../sdk/filing-dates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AA+BH;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACxC,MAAiB,EACjB,EAAE,UAAU,EAAE,SAAS,GAAG,KAAK,EAA6B;IAE5D,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,GAAG,CAA4B,oBAAoB,EAAE,SAAS,EAAE,EAAE,SAAS,EAAE,CAAC,CAAA;IAExG,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,KAAK,UAAU,CAAC,CAAA;AACnE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAoC,EAAE,QAA2B;IACrG,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAA;IAExE,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,qEAAqE,QAAQ,GAAG,CAAC,CAAA;IAClG,CAAC;IAED,IAAI,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAE,CAAA;IAEzB,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;QAC9B,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;YAClF,MAAM,GAAG,KAAK,CAAA;QACf,CAAC;IACF,CAAC;IAED,OAAO,MAAM,CAAC,UAAU,CAAA;AACzB,CAAC"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software.
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `filing_landscape` reader (2a Task 9) — the FOUR PRE-REGISTERED ACCEPTANCE GATES this whole phase
|
|
7
|
+
* is judged by. See `filing-landscape.test.ts` for the gate tests; this module is only the reader.
|
|
8
|
+
*
|
|
9
|
+
* Coverage check (the meaning-of-zero rule): a queried block counts as SURVEYED only when its res-6
|
|
10
|
+
* coverage cell is present in `layer_coverage` (via `readLayerCoverage`) — `undefined` means the area
|
|
11
|
+
* was never surveyed, and the block is reported in `unknown_block_count`, NEVER folded into a
|
|
12
|
+
* zero-filing claim.
|
|
13
|
+
*
|
|
14
|
+
* - For a `geoids` query, the candidate res-9 cell is read off the block's OWN `bdc_availability`
|
|
15
|
+
* rows — a geoid with zero rows has no derivable cell at all (never guessed, matching the builder's
|
|
16
|
+
* "unknown geoid" discipline in `build-bdc.ts`), so it falls straight to unknown.
|
|
17
|
+
* - For an `h3Cells` query, the caller supplies the res-9 cell directly, so coverage can be checked
|
|
18
|
+
* even for a cell with no filing rows of its own — a genuine "surveyed, zero providers here" result,
|
|
19
|
+
* the meaning-of-zero rule's POSITIVE case (covered but empty is not the same as never surveyed).
|
|
20
|
+
*
|
|
21
|
+
* Res-9 → res-6 parent reconstruction is deliberately NOT `@mailwoman/spatial`'s `expandH3Cell` — that
|
|
22
|
+
* helper's left-shift reconstruction only round-trips a short cell that was shortened AT resolution
|
|
23
|
+
* 15 (the address-id spine); fed a resolution-9 short cell it silently produces a full index
|
|
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}.
|
|
31
|
+
*
|
|
32
|
+
* This same formula is exactly what `build-bdc.ts` MUST use (and does, after fix round 1) to derive the
|
|
33
|
+
* coverage cell it writes at build time — H3's cell hierarchy is not geometrically exact, so a
|
|
34
|
+
* `latLngToCell(centroid, 6)` computed independently of the stored res-9 cell disagrees with
|
|
35
|
+
* `cellToParent(res9Cell, 6)` for a real fraction of points (verified ~6% over CONUS). Builder and
|
|
36
|
+
* reader deriving the res-6 parent differently was a real bug (fix round 1): a genuinely-surveyed block
|
|
37
|
+
* (real rows, real `layer_coverage` entry) could read back as `unknown_block_count` while its own rows
|
|
38
|
+
* still populated `filings` — a self-contradiction. `filings` is now scoped to units that PASS the
|
|
39
|
+
* coverage check (see the `surveyedUnits` accumulator below) precisely so that can't happen again: a
|
|
40
|
+
* block excluded from `surveyed_block_count` never contributes to `filings` either.
|
|
41
|
+
*/
|
|
42
|
+
import type { DatabaseClient } from "@mailwoman/core/kysley/client";
|
|
43
|
+
import { type BDCDatabase } from "../schema.ts";
|
|
44
|
+
/**
|
|
45
|
+
* Exactly one of `geoids` or `h3Cells` is required — `filingLandscape` throws otherwise.
|
|
46
|
+
*/
|
|
47
|
+
export interface FilingLandscapeQuery {
|
|
48
|
+
geoids?: string[];
|
|
49
|
+
h3Cells?: number[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* One provider/technology/speed-bucket group's block count within the query — `block_count` is the number of DISTINCT
|
|
53
|
+
* queried blocks carrying this exact combination, never a raw row count. A block can carry multiple `bdc_availability`
|
|
54
|
+
* rows for the SAME (provider_id, technology_code) pair even in the DEFAULT (non-`includeLocationIDs`) build mode:
|
|
55
|
+
* `build-bdc.ts`'s materialize-time collapse merges to one row per distinct (geoid, provider_id, technology_code,
|
|
56
|
+
* speeds, low_latency, business_residential_code) tuple, not one row per (geoid, provider_id, technology_code) triple —
|
|
57
|
+
* so Broadband Serviceable Locations at the same triple with DIFFERING speeds/flags survive as separate rows and can
|
|
58
|
+
* land in different `speed_bucket`s here (see that file's docstring). This `block_count`'s DISTINCT is exactly what
|
|
59
|
+
* keeps that from double-counting the block itself when it does.
|
|
60
|
+
*/
|
|
61
|
+
export interface ProviderFilingSummary {
|
|
62
|
+
provider_id: number;
|
|
63
|
+
technology_code: number;
|
|
64
|
+
speed_bucket: string;
|
|
65
|
+
block_count: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The queried landscape: ALWAYS vintage-stamped (from `layer_manifest.sourceVintage`) and ALWAYS reports its unknown
|
|
69
|
+
* blocks — `unknown_block_count` is reported, never zeroed, and never evidence of "no providers file here."
|
|
70
|
+
*/
|
|
71
|
+
export interface FilingLandscape {
|
|
72
|
+
vintage: string;
|
|
73
|
+
surveyed_block_count: number;
|
|
74
|
+
unknown_block_count: number;
|
|
75
|
+
filings: ProviderFilingSummary[];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* `speed_bucket` label for a block whose `max_advertised_download_speed` is below
|
|
79
|
+
* {@link BDC_SPEED_BUCKET_THRESHOLD_25_MBPS} Mbps.
|
|
80
|
+
*/
|
|
81
|
+
export declare const BDC_SPEED_BUCKET_UNDER_25 = "under-25";
|
|
82
|
+
/**
|
|
83
|
+
* `speed_bucket` label for `BDC_SPEED_BUCKET_THRESHOLD_25_MBPS <= speed < BDC_SPEED_BUCKET_THRESHOLD_100_MBPS`.
|
|
84
|
+
*/
|
|
85
|
+
export declare const BDC_SPEED_BUCKET_25_100 = "25-100";
|
|
86
|
+
/**
|
|
87
|
+
* `speed_bucket` label for `BDC_SPEED_BUCKET_THRESHOLD_100_MBPS <= speed < BDC_SPEED_BUCKET_THRESHOLD_GIGABIT_MBPS`.
|
|
88
|
+
*/
|
|
89
|
+
export declare const BDC_SPEED_BUCKET_100_1000 = "100-1000";
|
|
90
|
+
/**
|
|
91
|
+
* `speed_bucket` label for a block whose `max_advertised_download_speed` is at or above
|
|
92
|
+
* {@link BDC_SPEED_BUCKET_THRESHOLD_GIGABIT_MBPS} Mbps.
|
|
93
|
+
*/
|
|
94
|
+
export declare const BDC_SPEED_BUCKET_GIGABIT = "gigabit";
|
|
95
|
+
/**
|
|
96
|
+
* Upper-exclusive Mbps boundary between {@link BDC_SPEED_BUCKET_UNDER_25} and {@link BDC_SPEED_BUCKET_25_100}.
|
|
97
|
+
*/
|
|
98
|
+
export declare const BDC_SPEED_BUCKET_THRESHOLD_25_MBPS = 25;
|
|
99
|
+
/**
|
|
100
|
+
* Upper-exclusive Mbps boundary between {@link BDC_SPEED_BUCKET_25_100} and {@link BDC_SPEED_BUCKET_100_1000}.
|
|
101
|
+
*/
|
|
102
|
+
export declare const BDC_SPEED_BUCKET_THRESHOLD_100_MBPS = 100;
|
|
103
|
+
/**
|
|
104
|
+
* Mbps boundary at/above which a block is bucketed {@link BDC_SPEED_BUCKET_GIGABIT}.
|
|
105
|
+
*/
|
|
106
|
+
export declare const BDC_SPEED_BUCKET_THRESHOLD_GIGABIT_MBPS = 1000;
|
|
107
|
+
/**
|
|
108
|
+
* Pure mirror of the SQL `CASE` expression below ({@link speedBucketCaseSQL}) — same thresholds, same labels, exported
|
|
109
|
+
* so the boundary logic can be asserted directly without a database round trip.
|
|
110
|
+
*/
|
|
111
|
+
export declare function speedBucketForDownloadSpeed(maxAdvertisedDownloadSpeed: number): string;
|
|
112
|
+
/**
|
|
113
|
+
* Reconstruct the res-6 ancestor of a res-9 short-cell int WITHOUT a centroid — see the module docstring for why this
|
|
114
|
+
* isn't `@mailwoman/spatial`'s `expandH3Cell`. Exported so tests can assert this agrees, cell-for-cell, with
|
|
115
|
+
* `build-bdc.ts`'s own coverage-cell derivation (the two MUST share this exact formula — see that file's docstring).
|
|
116
|
+
*/
|
|
117
|
+
export declare function res9ShortCellToRes6Parent(h3CellShortInt: number): number;
|
|
118
|
+
/**
|
|
119
|
+
* Read the provider/technology/speed-bucket filing census over a set of queried blocks (by `geoid` or by `h3Cell`,
|
|
120
|
+
* never both). Always vintage-stamped; always throws on a broken manifest rather than answering unstamped; a queried
|
|
121
|
+
* block with no coverage evidence is reported in `unknown_block_count` and never folded into a zero-filing claim.
|
|
122
|
+
*/
|
|
123
|
+
export declare function filingLandscape(db: DatabaseClient<BDCDatabase>, query: FilingLandscapeQuery): Promise<FilingLandscape>;
|
|
124
|
+
//# sourceMappingURL=filing-landscape.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filing-landscape.d.ts","sourceRoot":"","sources":["../../sdk/filing-landscape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAMnE,OAAO,EAAiD,KAAK,WAAW,EAAE,MAAM,cAAc,CAAA;AAE9F;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,qBAAqB;IACrC,WAAW,EAAE,MAAM,CAAA;IACnB,eAAe,EAAE,MAAM,CAAA;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC/B,OAAO,EAAE,MAAM,CAAA;IACf,oBAAoB,EAAE,MAAM,CAAA;IAC5B,mBAAmB,EAAE,MAAM,CAAA;IAC3B,OAAO,EAAE,qBAAqB,EAAE,CAAA;CAChC;AAED;;;GAGG;AACH,eAAO,MAAM,yBAAyB,aAAa,CAAA;AAEnD;;GAEG;AACH,eAAO,MAAM,uBAAuB,WAAW,CAAA;AAE/C;;GAEG;AACH,eAAO,MAAM,yBAAyB,aAAa,CAAA;AAEnD;;;GAGG;AACH,eAAO,MAAM,wBAAwB,YAAY,CAAA;AAEjD;;GAEG;AACH,eAAO,MAAM,kCAAkC,KAAK,CAAA;AAEpD;;GAEG;AACH,eAAO,MAAM,mCAAmC,MAAM,CAAA;AAEtD;;GAEG;AACH,eAAO,MAAM,uCAAuC,OAAO,CAAA;AAE3D;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,0BAA0B,EAAE,MAAM,GAAG,MAAM,CAQtF;AAqBD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAMxE;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACpC,EAAE,EAAE,cAAc,CAAC,WAAW,CAAC,EAC/B,KAAK,EAAE,oBAAoB,GACzB,OAAO,CAAC,eAAe,CAAC,CA6G1B"}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software.
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `filing_landscape` reader (2a Task 9) — the FOUR PRE-REGISTERED ACCEPTANCE GATES this whole phase
|
|
7
|
+
* is judged by. See `filing-landscape.test.ts` for the gate tests; this module is only the reader.
|
|
8
|
+
*
|
|
9
|
+
* Coverage check (the meaning-of-zero rule): a queried block counts as SURVEYED only when its res-6
|
|
10
|
+
* coverage cell is present in `layer_coverage` (via `readLayerCoverage`) — `undefined` means the area
|
|
11
|
+
* was never surveyed, and the block is reported in `unknown_block_count`, NEVER folded into a
|
|
12
|
+
* zero-filing claim.
|
|
13
|
+
*
|
|
14
|
+
* - For a `geoids` query, the candidate res-9 cell is read off the block's OWN `bdc_availability`
|
|
15
|
+
* rows — a geoid with zero rows has no derivable cell at all (never guessed, matching the builder's
|
|
16
|
+
* "unknown geoid" discipline in `build-bdc.ts`), so it falls straight to unknown.
|
|
17
|
+
* - For an `h3Cells` query, the caller supplies the res-9 cell directly, so coverage can be checked
|
|
18
|
+
* even for a cell with no filing rows of its own — a genuine "surveyed, zero providers here" result,
|
|
19
|
+
* the meaning-of-zero rule's POSITIVE case (covered but empty is not the same as never surveyed).
|
|
20
|
+
*
|
|
21
|
+
* Res-9 → res-6 parent reconstruction is deliberately NOT `@mailwoman/spatial`'s `expandH3Cell` — that
|
|
22
|
+
* helper's left-shift reconstruction only round-trips a short cell that was shortened AT resolution
|
|
23
|
+
* 15 (the address-id spine); fed a resolution-9 short cell it silently produces a full index
|
|
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}.
|
|
31
|
+
*
|
|
32
|
+
* This same formula is exactly what `build-bdc.ts` MUST use (and does, after fix round 1) to derive the
|
|
33
|
+
* coverage cell it writes at build time — H3's cell hierarchy is not geometrically exact, so a
|
|
34
|
+
* `latLngToCell(centroid, 6)` computed independently of the stored res-9 cell disagrees with
|
|
35
|
+
* `cellToParent(res9Cell, 6)` for a real fraction of points (verified ~6% over CONUS). Builder and
|
|
36
|
+
* reader deriving the res-6 parent differently was a real bug (fix round 1): a genuinely-surveyed block
|
|
37
|
+
* (real rows, real `layer_coverage` entry) could read back as `unknown_block_count` while its own rows
|
|
38
|
+
* still populated `filings` — a self-contradiction. `filings` is now scoped to units that PASS the
|
|
39
|
+
* coverage check (see the `surveyedUnits` accumulator below) precisely so that can't happen again: a
|
|
40
|
+
* block excluded from `surveyed_block_count` never contributes to `filings` either.
|
|
41
|
+
*/
|
|
42
|
+
import { readLayerCoverage, readLayerManifest } from "@mailwoman/core/layers";
|
|
43
|
+
import { shortCellToInt } from "@mailwoman/spatial";
|
|
44
|
+
import { cellToParent } from "h3-js";
|
|
45
|
+
import { sql } from "kysely";
|
|
46
|
+
import { BDC_COVERAGE_H3_RESOLUTION, BDC_H3_RESOLUTION } from "../schema.js";
|
|
47
|
+
/**
|
|
48
|
+
* `speed_bucket` label for a block whose `max_advertised_download_speed` is below
|
|
49
|
+
* {@link BDC_SPEED_BUCKET_THRESHOLD_25_MBPS} Mbps.
|
|
50
|
+
*/
|
|
51
|
+
export const BDC_SPEED_BUCKET_UNDER_25 = "under-25";
|
|
52
|
+
/**
|
|
53
|
+
* `speed_bucket` label for `BDC_SPEED_BUCKET_THRESHOLD_25_MBPS <= speed < BDC_SPEED_BUCKET_THRESHOLD_100_MBPS`.
|
|
54
|
+
*/
|
|
55
|
+
export const BDC_SPEED_BUCKET_25_100 = "25-100";
|
|
56
|
+
/**
|
|
57
|
+
* `speed_bucket` label for `BDC_SPEED_BUCKET_THRESHOLD_100_MBPS <= speed < BDC_SPEED_BUCKET_THRESHOLD_GIGABIT_MBPS`.
|
|
58
|
+
*/
|
|
59
|
+
export const BDC_SPEED_BUCKET_100_1000 = "100-1000";
|
|
60
|
+
/**
|
|
61
|
+
* `speed_bucket` label for a block whose `max_advertised_download_speed` is at or above
|
|
62
|
+
* {@link BDC_SPEED_BUCKET_THRESHOLD_GIGABIT_MBPS} Mbps.
|
|
63
|
+
*/
|
|
64
|
+
export const BDC_SPEED_BUCKET_GIGABIT = "gigabit";
|
|
65
|
+
/**
|
|
66
|
+
* Upper-exclusive Mbps boundary between {@link BDC_SPEED_BUCKET_UNDER_25} and {@link BDC_SPEED_BUCKET_25_100}.
|
|
67
|
+
*/
|
|
68
|
+
export const BDC_SPEED_BUCKET_THRESHOLD_25_MBPS = 25;
|
|
69
|
+
/**
|
|
70
|
+
* Upper-exclusive Mbps boundary between {@link BDC_SPEED_BUCKET_25_100} and {@link BDC_SPEED_BUCKET_100_1000}.
|
|
71
|
+
*/
|
|
72
|
+
export const BDC_SPEED_BUCKET_THRESHOLD_100_MBPS = 100;
|
|
73
|
+
/**
|
|
74
|
+
* Mbps boundary at/above which a block is bucketed {@link BDC_SPEED_BUCKET_GIGABIT}.
|
|
75
|
+
*/
|
|
76
|
+
export const BDC_SPEED_BUCKET_THRESHOLD_GIGABIT_MBPS = 1000;
|
|
77
|
+
/**
|
|
78
|
+
* Pure mirror of the SQL `CASE` expression below ({@link speedBucketCaseSQL}) — same thresholds, same labels, exported
|
|
79
|
+
* so the boundary logic can be asserted directly without a database round trip.
|
|
80
|
+
*/
|
|
81
|
+
export function speedBucketForDownloadSpeed(maxAdvertisedDownloadSpeed) {
|
|
82
|
+
if (maxAdvertisedDownloadSpeed < BDC_SPEED_BUCKET_THRESHOLD_25_MBPS)
|
|
83
|
+
return BDC_SPEED_BUCKET_UNDER_25;
|
|
84
|
+
if (maxAdvertisedDownloadSpeed < BDC_SPEED_BUCKET_THRESHOLD_100_MBPS)
|
|
85
|
+
return BDC_SPEED_BUCKET_25_100;
|
|
86
|
+
if (maxAdvertisedDownloadSpeed < BDC_SPEED_BUCKET_THRESHOLD_GIGABIT_MBPS)
|
|
87
|
+
return BDC_SPEED_BUCKET_100_1000;
|
|
88
|
+
return BDC_SPEED_BUCKET_GIGABIT;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The same bucketing as {@link speedBucketForDownloadSpeed}, expressed as a `CASE` over `max_advertised_download_speed`
|
|
92
|
+
* so the GROUP BY below can group directly on the bucket.
|
|
93
|
+
*/
|
|
94
|
+
const speedBucketCaseSQL = sql `CASE
|
|
95
|
+
WHEN max_advertised_download_speed < ${BDC_SPEED_BUCKET_THRESHOLD_25_MBPS} THEN ${BDC_SPEED_BUCKET_UNDER_25}
|
|
96
|
+
WHEN max_advertised_download_speed < ${BDC_SPEED_BUCKET_THRESHOLD_100_MBPS} THEN ${BDC_SPEED_BUCKET_25_100}
|
|
97
|
+
WHEN max_advertised_download_speed < ${BDC_SPEED_BUCKET_THRESHOLD_GIGABIT_MBPS} THEN ${BDC_SPEED_BUCKET_100_1000}
|
|
98
|
+
ELSE ${BDC_SPEED_BUCKET_GIGABIT}
|
|
99
|
+
END`;
|
|
100
|
+
/**
|
|
101
|
+
* `BDCDatabase extends LayerContractDatabase` structurally, but Kysely's `transaction()` makes `Kysely<DB>` INVARIANT
|
|
102
|
+
* in `DB` — same narrowing cast as `build-bdc.ts`'s `asContractDB`.
|
|
103
|
+
*/
|
|
104
|
+
function asContractDB(kdb) {
|
|
105
|
+
return kdb;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Reconstruct the res-6 ancestor of a res-9 short-cell int WITHOUT a centroid — see the module docstring for why this
|
|
109
|
+
* isn't `@mailwoman/spatial`'s `expandH3Cell`. Exported so tests can assert this agrees, cell-for-cell, with
|
|
110
|
+
* `build-bdc.ts`'s own coverage-cell derivation (the two MUST share this exact formula — see that file's docstring).
|
|
111
|
+
*/
|
|
112
|
+
export function res9ShortCellToRes6Parent(h3CellShortInt) {
|
|
113
|
+
const shortHex = h3CellShortInt.toString(16).padStart(13, "0");
|
|
114
|
+
const fullCell = `8${BDC_H3_RESOLUTION.toString(16)}${shortHex}`;
|
|
115
|
+
const parentCell = cellToParent(fullCell, BDC_COVERAGE_H3_RESOLUTION);
|
|
116
|
+
return shortCellToInt(parentCell);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Read the provider/technology/speed-bucket filing census over a set of queried blocks (by `geoid` or by `h3Cell`,
|
|
120
|
+
* never both). Always vintage-stamped; always throws on a broken manifest rather than answering unstamped; a queried
|
|
121
|
+
* block with no coverage evidence is reported in `unknown_block_count` and never folded into a zero-filing claim.
|
|
122
|
+
*/
|
|
123
|
+
export async function filingLandscape(db, query) {
|
|
124
|
+
const queryModeCount = (query.geoids ? 1 : 0) + (query.h3Cells ? 1 : 0);
|
|
125
|
+
if (queryModeCount !== 1) {
|
|
126
|
+
throw new Error("filingLandscape: exactly one of `geoids` or `h3Cells` is required");
|
|
127
|
+
}
|
|
128
|
+
// `[]` is truthy, so it passes the XOR check above undetected — without this guard an empty array sails
|
|
129
|
+
// straight through to a vacuous all-zero landscape (surveyed_block_count: 0, unknown_block_count: 0, no
|
|
130
|
+
// filings), which reads exactly like a real "nothing queried" answer instead of the malformed-query error
|
|
131
|
+
// it should be. Checked before the manifest read so a bad query fails fast without even opening the db further.
|
|
132
|
+
if (!(query.geoids ?? query.h3Cells).length) {
|
|
133
|
+
throw new Error("filingLandscape: `geoids`/`h3Cells` must not be an empty array");
|
|
134
|
+
}
|
|
135
|
+
// Read (and validate) the manifest FIRST — a broken/missing manifest must throw before any block is
|
|
136
|
+
// classified, never fall through to an "unstamped" answer (gate 4).
|
|
137
|
+
const manifest = await readLayerManifest(asContractDB(db));
|
|
138
|
+
const requestedUnits = query.geoids ?? query.h3Cells;
|
|
139
|
+
const unitColumn = query.geoids ? "geoid" : "h3_cell";
|
|
140
|
+
// Candidate res-9 cell per requested unit. `h3Cells` queries already carry the cell directly;
|
|
141
|
+
// `geoids` queries can only derive one from the block's OWN rows — a geoid with none has no
|
|
142
|
+
// candidate at all (never guessed), so it falls straight to unknown below.
|
|
143
|
+
const candidateCellByUnit = new Map();
|
|
144
|
+
if (query.geoids) {
|
|
145
|
+
const rows = await db
|
|
146
|
+
.selectFrom("bdc_availability")
|
|
147
|
+
.select(["geoid", "h3_cell"])
|
|
148
|
+
.where("geoid", "in", query.geoids)
|
|
149
|
+
.groupBy(["geoid", "h3_cell"])
|
|
150
|
+
.execute();
|
|
151
|
+
for (const row of rows) {
|
|
152
|
+
candidateCellByUnit.set(row.geoid, row.h3_cell);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
for (const cell of query.h3Cells) {
|
|
157
|
+
candidateCellByUnit.set(cell, cell);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
let surveyedBlockCount = 0;
|
|
161
|
+
let unknownBlockCount = 0;
|
|
162
|
+
// Only units that PASS the coverage check feed the census below — a unit with rows but no coverage evidence
|
|
163
|
+
// (a corrupted/inconsistent db — see filing-landscape.test.ts's "coverage row deleted" gate) is `unknown`, and
|
|
164
|
+
// its rows must not leak into `filings` either: `surveyed_block_count` and the blocks backing `filings` must
|
|
165
|
+
// always agree, or a caller cross-referencing the two gets a contradiction (an "unknown" block whose filings
|
|
166
|
+
// still show up looks exactly like the false-negative bug this reader exists to prevent).
|
|
167
|
+
const surveyedUnits = [];
|
|
168
|
+
for (const unit of requestedUnits) {
|
|
169
|
+
const candidateCell = candidateCellByUnit.get(unit);
|
|
170
|
+
if (candidateCell === undefined) {
|
|
171
|
+
unknownBlockCount++;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const res6Parent = res9ShortCellToRes6Parent(candidateCell);
|
|
175
|
+
const coverage = await readLayerCoverage(asContractDB(db), res6Parent);
|
|
176
|
+
if (coverage === undefined) {
|
|
177
|
+
unknownBlockCount++;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
surveyedBlockCount++;
|
|
181
|
+
surveyedUnits.push(unit);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
let filings = [];
|
|
185
|
+
if (surveyedUnits.length) {
|
|
186
|
+
let filingsQuery = db
|
|
187
|
+
.selectFrom("bdc_availability")
|
|
188
|
+
.select([
|
|
189
|
+
"provider_id",
|
|
190
|
+
"technology_code",
|
|
191
|
+
speedBucketCaseSQL.as("speed_bucket"),
|
|
192
|
+
(eb) => eb.fn.count(unitColumn).distinct().as("block_count"),
|
|
193
|
+
])
|
|
194
|
+
.groupBy(["provider_id", "technology_code", speedBucketCaseSQL])
|
|
195
|
+
.orderBy("provider_id")
|
|
196
|
+
.orderBy("technology_code")
|
|
197
|
+
.orderBy(speedBucketCaseSQL);
|
|
198
|
+
filingsQuery = query.geoids
|
|
199
|
+
? filingsQuery.where("geoid", "in", surveyedUnits)
|
|
200
|
+
: filingsQuery.where("h3_cell", "in", surveyedUnits);
|
|
201
|
+
const filingsRows = await filingsQuery.execute();
|
|
202
|
+
filings = filingsRows.map((row) => ({
|
|
203
|
+
provider_id: row.provider_id,
|
|
204
|
+
technology_code: row.technology_code,
|
|
205
|
+
speed_bucket: row.speed_bucket,
|
|
206
|
+
block_count: row.block_count,
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
vintage: manifest.sourceVintage,
|
|
211
|
+
surveyed_block_count: surveyedBlockCount,
|
|
212
|
+
unknown_block_count: unknownBlockCount,
|
|
213
|
+
filings,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=filing-landscape.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filing-landscape.js","sourceRoot":"","sources":["../../sdk/filing-landscape.ts"],"names":[],"mappings":"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"}
|