@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
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAGH,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAA8B,MAAM,wBAAwB,CAAA;AACzG,OAAO,EAAE,cAAc,EAAe,MAAM,oBAAoB,CAAA;AAChE,OAAO,EAAE,YAAY,EAAE,MAAM,OAAO,CAAA;AACpC,OAAO,EAAE,GAAG,EAAe,MAAM,QAAQ,CAAA;AAEzC,OAAO,EAAE,0BAA0B,EAAE,iBAAiB,EAAoB,MAAM,cAAc,CAAA;AAsC9F;;;GAGG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,UAAU,CAAA;AAEnD;;GAEG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,QAAQ,CAAA;AAE/C;;GAEG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,UAAU,CAAA;AAEnD;;;GAGG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,SAAS,CAAA;AAEjD;;GAEG;AACH,MAAM,CAAC,MAAM,kCAAkC,GAAG,EAAE,CAAA;AAEpD;;GAEG;AACH,MAAM,CAAC,MAAM,mCAAmC,GAAG,GAAG,CAAA;AAEtD;;GAEG;AACH,MAAM,CAAC,MAAM,uCAAuC,GAAG,IAAI,CAAA;AAE3D;;;GAGG;AACH,MAAM,UAAU,2BAA2B,CAAC,0BAAkC;IAC7E,IAAI,0BAA0B,GAAG,kCAAkC;QAAE,OAAO,yBAAyB,CAAA;IAErG,IAAI,0BAA0B,GAAG,mCAAmC;QAAE,OAAO,uBAAuB,CAAA;IAEpG,IAAI,0BAA0B,GAAG,uCAAuC;QAAE,OAAO,yBAAyB,CAAA;IAE1G,OAAO,wBAAwB,CAAA;AAChC,CAAC;AAED;;;GAGG;AACH,MAAM,kBAAkB,GAAG,GAAG,CAAQ;wCACE,kCAAkC,SAAS,yBAAyB;wCACpE,mCAAmC,SAAS,uBAAuB;wCACnE,uCAAuC,SAAS,yBAAyB;QACzG,wBAAwB;IAC5B,CAAA;AAEJ;;;GAGG;AACH,SAAS,YAAY,CAAC,GAAgC;IACrD,OAAO,GAA+C,CAAA;AACvD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,cAAsB;IAC/D,MAAM,QAAQ,GAAG,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAC,CAAA;IAC9D,MAAM,QAAQ,GAAG,IAAI,iBAAiB,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,QAAQ,EAAY,CAAA;IAC1E,MAAM,UAAU,GAAG,YAAY,CAAC,QAAQ,EAAE,0BAA0B,CAAW,CAAA;IAE/E,OAAO,cAAc,CAAC,UAAU,CAAC,CAAA;AAClC,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACpC,EAA+B,EAC/B,KAA2B;IAE3B,MAAM,cAAc,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAEvE,IAAI,cAAc,KAAK,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,mEAAmE,CAAC,CAAA;IACrF,CAAC;IAED,wGAAwG;IACxG,wGAAwG;IACxG,0GAA0G;IAC1G,gHAAgH;IAChH,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,OAAO,CAAE,CAAC,MAAM,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CAAC,gEAAgE,CAAC,CAAA;IAClF,CAAC;IAED,oGAAoG;IACpG,oEAAoE;IACpE,MAAM,QAAQ,GAAG,MAAM,iBAAiB,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAA;IAE1D,MAAM,cAAc,GAAmC,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,OAAQ,CAAA;IACrF,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAE,OAAiB,CAAC,CAAC,CAAE,SAAmB,CAAA;IAE3E,8FAA8F;IAC9F,4FAA4F;IAC5F,2EAA2E;IAC3E,MAAM,mBAAmB,GAAG,IAAI,GAAG,EAA2B,CAAA;IAE9D,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QAClB,MAAM,IAAI,GAAG,MAAM,EAAE;aACnB,UAAU,CAAC,kBAAkB,CAAC;aAC9B,MAAM,CAAC,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;aAC5B,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC;aAClC,OAAO,CAAC,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;aAC7B,OAAO,EAAE,CAAA;QAEX,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACxB,mBAAmB,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC,CAAA;QAChD,CAAC;IACF,CAAC;SAAM,CAAC;QACP,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,OAAQ,EAAE,CAAC;YACnC,mBAAmB,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;QACpC,CAAC;IACF,CAAC;IAED,IAAI,kBAAkB,GAAG,CAAC,CAAA;IAC1B,IAAI,iBAAiB,GAAG,CAAC,CAAA;IACzB,4GAA4G;IAC5G,+GAA+G;IAC/G,6GAA6G;IAC7G,6GAA6G;IAC7G,0FAA0F;IAC1F,MAAM,aAAa,GAA2B,EAAE,CAAA;IAEhD,KAAK,MAAM,IAAI,IAAI,cAAc,EAAE,CAAC;QACnC,MAAM,aAAa,GAAG,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QAEnD,IAAI,aAAa,KAAK,SAAS,EAAE,CAAC;YACjC,iBAAiB,EAAE,CAAA;YAEnB,SAAQ;QACT,CAAC;QAED,MAAM,UAAU,GAAG,yBAAyB,CAAC,aAAa,CAAC,CAAA;QAC3D,MAAM,QAAQ,GAAG,MAAM,iBAAiB,CAAC,YAAY,CAAC,EAAE,CAAC,EAAE,UAAU,CAAC,CAAA;QAEtE,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC5B,iBAAiB,EAAE,CAAA;QACpB,CAAC;aAAM,CAAC;YACP,kBAAkB,EAAE,CAAA;YACpB,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACzB,CAAC;IACF,CAAC;IAED,IAAI,OAAO,GAA4B,EAAE,CAAA;IAEzC,IAAI,aAAa,CAAC,MAAM,EAAE,CAAC;QAC1B,IAAI,YAAY,GAAG,EAAE;aACnB,UAAU,CAAC,kBAAkB,CAAC;aAC9B,MAAM,CAAC;YACP,aAAa;YACb,iBAAiB;YACjB,kBAAkB,CAAC,EAAE,CAAC,cAAc,CAAC;YACrC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,CAAS,UAAU,CAAC,CAAC,QAAQ,EAAE,CAAC,EAAE,CAAC,aAAa,CAAC;SACpE,CAAC;aACD,OAAO,CAAC,CAAC,aAAa,EAAE,iBAAiB,EAAE,kBAAkB,CAAC,CAAC;aAC/D,OAAO,CAAC,aAAa,CAAC;aACtB,OAAO,CAAC,iBAAiB,CAAC;aAC1B,OAAO,CAAC,kBAAkB,CAAC,CAAA;QAE7B,YAAY,GAAG,KAAK,CAAC,MAAM;YAC1B,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,aAAyB,CAAC;YAC9D,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,SAAS,EAAE,IAAI,EAAE,aAAyB,CAAC,CAAA;QAEjE,MAAM,WAAW,GAAG,MAAM,YAAY,CAAC,OAAO,EAAE,CAAA;QAEhD,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YACnC,WAAW,EAAE,GAAG,CAAC,WAAW;YAC5B,eAAe,EAAE,GAAG,CAAC,eAAe;YACpC,YAAY,EAAE,GAAG,CAAC,YAAY;YAC9B,WAAW,EAAE,GAAG,CAAC,WAAW;SAC5B,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,OAAO;QACN,OAAO,EAAE,QAAQ,CAAC,aAAa;QAC/B,oBAAoB,EAAE,kBAAkB;QACxC,mBAAmB,EAAE,iBAAiB;QACtC,OAAO;KACP,CAAA;AACF,CAAC"}