@bitbaum/geo-kit 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +112 -2
- package/dist/boundaries.d.ts +4 -0
- package/dist/boundaries.js +99 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +8 -0
- package/dist/http.d.ts +6 -0
- package/dist/http.js +93 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/manifests.d.ts +3 -0
- package/dist/manifests.js +22 -0
- package/dist/resources.d.ts +14 -0
- package/dist/resources.js +270 -0
- package/dist/spatial.d.ts +4 -0
- package/dist/spatial.js +21 -0
- package/dist/types.d.ts +216 -0
- package/dist/types.js +10 -0
- package/dist/validation.d.ts +17 -0
- package/dist/validation.js +375 -0
- package/package.json +39 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bitbaum
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,113 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@bitbaum/geo-kit`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Country-neutral contracts for sharing versioned geography data across
|
|
4
|
+
applications. The package owns no country packs, political claims, source
|
|
5
|
+
choices, personal data, HTTP server, or rendering framework.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
The Git repository is the install source until npm publishing is bootstrapped.
|
|
10
|
+
Pin the reviewed v0.1.1 source commit for a reproducible install:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
pnpm add github:bitbaum/geo-kit#a337aec928cf7804fecf459c308d366b90f4b2fa
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The npm workflow uses npm Trusted Publishing and provenance. The first npm
|
|
17
|
+
publish and publisher trust setup require a maintainer-held passkey; publishing
|
|
18
|
+
stays disabled until that is complete.
|
|
19
|
+
|
|
20
|
+
## Ownership
|
|
21
|
+
|
|
22
|
+
- **Solon** is the authoritative owner of public places, source records,
|
|
23
|
+
historical assertions, decisions, and published boundary resources.
|
|
24
|
+
- **OrangeCat** remains the owner of private residence and account data.
|
|
25
|
+
- **Substrata and other clients** consume public, versioned data and choose the
|
|
26
|
+
layers they need. They do not copy Solon's geography database.
|
|
27
|
+
- Application policy supplies accepted licences and controls whether a
|
|
28
|
+
proposal is approved. This package does not make that decision.
|
|
29
|
+
|
|
30
|
+
## Data loading
|
|
31
|
+
|
|
32
|
+
Publish a small manifest beside content-addressed GeoJSON or TopoJSON resources.
|
|
33
|
+
Each resource identifies its source, geography, open-ended layer and level keys,
|
|
34
|
+
validity period, optional map viewpoint, URL, SHA-256 digest, size and feature
|
|
35
|
+
count. Resources can reference multiple sources, with each source's licence and
|
|
36
|
+
attribution checked independently. Viewpoints have names, descriptions and
|
|
37
|
+
source references, so a client can explain which representation it displays.
|
|
38
|
+
A client asks for one or more
|
|
39
|
+
geography/resource IDs, a date, and (when needed) a viewpoint. The default view
|
|
40
|
+
includes only resources with no explicit viewpoint. `selectGeographyResources`
|
|
41
|
+
rejects broad accidental loads by default and enforces byte/resource limits
|
|
42
|
+
before any request is made.
|
|
43
|
+
|
|
44
|
+
`loadGeographyResources` fetches selected same-origin URLs only, without cookies
|
|
45
|
+
and without following redirects. It checks the decoded file size, SHA-256,
|
|
46
|
+
GeoJSON/TopoJSON structure and feature count before returning parsed data. It
|
|
47
|
+
accepts an injected cache, fetch implementation and abort signal so each app can
|
|
48
|
+
use its own HTTP cache, IndexedDB policy and retry interface. Requests are
|
|
49
|
+
bounded-concurrency and results use deterministic geography/kind/level/id order.
|
|
50
|
+
|
|
51
|
+
`loadGeographyManifest` uses the same transport protections for the manifest,
|
|
52
|
+
including a decoded byte limit enforced while streaming (64 KiB by default).
|
|
53
|
+
An abort cancels a stalled body and also stops loads served from a cache.
|
|
54
|
+
|
|
55
|
+
Resource selection can include a WGS84 `bbox` in west/south/east/north order.
|
|
56
|
+
Only intersecting resources with known bounds are selected. Resources without
|
|
57
|
+
bounds remain eligible, so missing metadata cannot silently hide coverage.
|
|
58
|
+
West greater than east represents an antimeridian crossing. Geographic, time,
|
|
59
|
+
viewpoint and download-budget selectors continue to apply together.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { loadGeographyManifest, loadGeographyResources } from '@bitbaum/geo-kit';
|
|
63
|
+
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
const options = {
|
|
66
|
+
baseUrl: window.location.origin,
|
|
67
|
+
licensePolicy: [{ spdx: 'CC0-1.0', requiresAttribution: false }],
|
|
68
|
+
signal: controller.signal, // AbortController owned by the application
|
|
69
|
+
};
|
|
70
|
+
const manifest = await loadGeographyManifest('/geography/manifest.json', options);
|
|
71
|
+
const layers = await loadGeographyResources(manifest, {
|
|
72
|
+
geographyIds: ['selected-geography'],
|
|
73
|
+
asOf: '2026-09-30', // the date displayed by the application
|
|
74
|
+
levelKeys: ['subdivision'],
|
|
75
|
+
bbox: [7, 46, 9, 48],
|
|
76
|
+
maxBytes: 512 * 1024,
|
|
77
|
+
}, options);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Layer keys and geographic IDs in this example come from the application's
|
|
81
|
+
data. Country packs, detailed subdivision datasets and a border drawing UI are
|
|
82
|
+
separate deliverables owned by the applications; this package does not invent
|
|
83
|
+
or publish them.
|
|
84
|
+
|
|
85
|
+
The library never reads the system clock. Pass the date displayed by the app to
|
|
86
|
+
support historical maps deterministically. An explicit viewpoint is opt-in;
|
|
87
|
+
the library does not treat any boundary representation as the default truth.
|
|
88
|
+
|
|
89
|
+
## Assertions and proposed boundaries
|
|
90
|
+
|
|
91
|
+
`AreaAssertion` follows Solon's sourced `claims`, `administers`, and `proposes`
|
|
92
|
+
records. `summarizeAreaAssertions` filters those records by an explicit date and
|
|
93
|
+
reports distinct claimants, administrators and proposers. Its `disputed` flag is
|
|
94
|
+
a mechanical summary of competing claims or a mismatch between claimant and
|
|
95
|
+
administrator; it is not a legal conclusion.
|
|
96
|
+
|
|
97
|
+
`validateBoundaryDraft` checks a proposal's polygon, WGS84 coordinates, closed
|
|
98
|
+
rings, input limits, source/license metadata and validity interval. Publication
|
|
99
|
+
stays blocked until the consuming app provides both a full geometry-topology
|
|
100
|
+
validator and an approval check for the draft's proposal. The draft contains a
|
|
101
|
+
shape and its provenance only; claims about who owns or administers it are
|
|
102
|
+
separate sourced assertions.
|
|
103
|
+
|
|
104
|
+
## Verify
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
pnpm install --frozen-lockfile
|
|
108
|
+
pnpm run verify
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Runtime code has no dependencies and targets modern browsers plus Node.js 20 or
|
|
112
|
+
later. The package ships JavaScript and declarations built from the TypeScript
|
|
113
|
+
source, so consumers do not need a TypeScript-aware runtime loader.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AreaAssertion, AreaPoliticalSummary, BoundaryDraft, BoundaryDraftValidation, BoundaryDraftValidationOptions, ISODate } from "./types.ts";
|
|
2
|
+
export declare function summarizeAreaAssertions(areaId: string, assertions: readonly AreaAssertion[], asOf: ISODate): AreaPoliticalSummary;
|
|
3
|
+
/** Validate a boundary submitted for a proposal; only the owning app can approve publication. */
|
|
4
|
+
export declare function validateBoundaryDraft(draft: BoundaryDraft, options: BoundaryDraftValidationOptions): BoundaryDraftValidation;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { GeographyError } from "./errors.js";
|
|
2
|
+
import { isISODate, isPlainObject, isStableKey, periodContains, validateAreaAssertions, validateGeometry, validPeriod, validUrl } from "./validation.js";
|
|
3
|
+
export function summarizeAreaAssertions(areaId, assertions, asOf) {
|
|
4
|
+
if (!isISODate(asOf))
|
|
5
|
+
throw new GeographyError("invalid_manifest", "asOf must be a real ISO date");
|
|
6
|
+
const assertionProblems = validateAreaAssertions(assertions);
|
|
7
|
+
if (assertionProblems.length) {
|
|
8
|
+
throw new GeographyError("invalid_resource", assertionProblems.slice(0, 8).join("; "));
|
|
9
|
+
}
|
|
10
|
+
const current = assertions.filter((assertion) => assertion.areaId === areaId &&
|
|
11
|
+
periodContains(assertion.validFrom, assertion.validTo, asOf));
|
|
12
|
+
const unique = (kind) => [...new Set(current.filter((item) => item.assertion === kind).map((item) => item.jurisdictionId))].sort();
|
|
13
|
+
const claimants = unique("claims");
|
|
14
|
+
const administrators = unique("administers");
|
|
15
|
+
const proposers = unique("proposes");
|
|
16
|
+
return {
|
|
17
|
+
claimants,
|
|
18
|
+
administrators,
|
|
19
|
+
proposers,
|
|
20
|
+
disputed: claimants.length > 1 ||
|
|
21
|
+
(claimants.length === 1 && administrators.some((id) => id !== claimants[0])),
|
|
22
|
+
unclaimed: claimants.length === 0,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Validate a boundary submitted for a proposal; only the owning app can approve publication. */
|
|
26
|
+
export function validateBoundaryDraft(draft, options) {
|
|
27
|
+
const problems = [];
|
|
28
|
+
if (!isStableKey(draft.id))
|
|
29
|
+
problems.push("draft id is invalid");
|
|
30
|
+
if (!isStableKey(draft.proposalId))
|
|
31
|
+
problems.push("proposalId is invalid");
|
|
32
|
+
if (!draft.title.trim())
|
|
33
|
+
problems.push("title is required");
|
|
34
|
+
if (!isStableKey(draft.geographyId) || !isStableKey(draft.kindKey)) {
|
|
35
|
+
problems.push("geographyId and kindKey must be stable keys");
|
|
36
|
+
}
|
|
37
|
+
if (!validPeriod(draft.validFrom, draft.validTo))
|
|
38
|
+
problems.push("validity period is invalid");
|
|
39
|
+
if (draft.geometry.type !== "Polygon" && draft.geometry.type !== "MultiPolygon") {
|
|
40
|
+
problems.push("a territory draft must be a Polygon or MultiPolygon");
|
|
41
|
+
}
|
|
42
|
+
const maxVertices = options.maxVertices ?? 100_000;
|
|
43
|
+
const geometryProblems = validateGeometry(draft.geometry, maxVertices);
|
|
44
|
+
problems.push(...geometryProblems);
|
|
45
|
+
const serialized = JSON.stringify(draft.geometry);
|
|
46
|
+
const byteSize = new TextEncoder().encode(serialized).byteLength;
|
|
47
|
+
const maxBytes = options.maxBytes ?? 5 * 1024 * 1024;
|
|
48
|
+
if (byteSize > maxBytes)
|
|
49
|
+
problems.push(`geometry is ${byteSize} bytes; limit is ${maxBytes}`);
|
|
50
|
+
const vertexCount = countPositions(draft.geometry);
|
|
51
|
+
const source = draft.source;
|
|
52
|
+
const rule = options.licensePolicy.find((candidate) => candidate.spdx === source.licenseSPDX);
|
|
53
|
+
if (!source.publisher.trim() || !source.dataset.trim() || !source.licenseSPDX.trim()) {
|
|
54
|
+
problems.push("boundary source needs a publisher, dataset, and license");
|
|
55
|
+
}
|
|
56
|
+
if (!rule) {
|
|
57
|
+
problems.push(`license ${source.licenseSPDX} is outside the supplied policy`);
|
|
58
|
+
}
|
|
59
|
+
else if (rule.requiresAttribution && !source.attribution?.trim()) {
|
|
60
|
+
problems.push(`license ${source.licenseSPDX} requires attribution`);
|
|
61
|
+
}
|
|
62
|
+
if (source.url && !validUrl(source.url, ["https:", "http:"]))
|
|
63
|
+
problems.push("source url must be HTTP(S)");
|
|
64
|
+
const topology = options.validateTopology?.(draft.geometry);
|
|
65
|
+
if (!options.validateTopology)
|
|
66
|
+
problems.push("a full topology validator is required before publication");
|
|
67
|
+
else if (!topology?.valid)
|
|
68
|
+
problems.push(...(topology?.problems?.length ? topology.problems : ["geometry topology is invalid"]));
|
|
69
|
+
if (!options.proposalApproved)
|
|
70
|
+
problems.push("the app must verify the proposal is approved before publication");
|
|
71
|
+
else if (!options.proposalApproved(draft.proposalId))
|
|
72
|
+
problems.push("proposal is not approved for publication");
|
|
73
|
+
return {
|
|
74
|
+
problems,
|
|
75
|
+
publishable: problems.length === 0,
|
|
76
|
+
vertexCount,
|
|
77
|
+
byteSize,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function countPositions(value) {
|
|
81
|
+
let count = 0;
|
|
82
|
+
const visit = (part) => {
|
|
83
|
+
if (!Array.isArray(part))
|
|
84
|
+
return;
|
|
85
|
+
if (part.length >= 2 && part.every((coordinate) => typeof coordinate === "number")) {
|
|
86
|
+
count += 1;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
for (const child of part)
|
|
90
|
+
visit(child);
|
|
91
|
+
};
|
|
92
|
+
if (isPlainObject(value)) {
|
|
93
|
+
if ("coordinates" in value)
|
|
94
|
+
visit(value.coordinates);
|
|
95
|
+
if (Array.isArray(value.geometries))
|
|
96
|
+
value.geometries.forEach(visit);
|
|
97
|
+
}
|
|
98
|
+
return count;
|
|
99
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare class GeographyError extends Error {
|
|
2
|
+
readonly code: "invalid_manifest" | "selection_required" | "selection_limit" | "invalid_resource" | "resource_fetch_failed" | "resource_too_large" | "invalid_boundary_draft";
|
|
3
|
+
constructor(code: "invalid_manifest" | "selection_required" | "selection_limit" | "invalid_resource" | "resource_fetch_failed" | "resource_too_large" | "invalid_boundary_draft", message: string);
|
|
4
|
+
}
|
package/dist/errors.js
ADDED
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { LoadResourcesOptions } from "./types.ts";
|
|
2
|
+
type FetchOptions = Pick<LoadResourcesOptions, "baseUrl" | "signal" | "fetcher">;
|
|
3
|
+
export declare function publicDataUrl(href: string, baseUrl: string): URL;
|
|
4
|
+
/** Shared bounded transport for manifests and geometry, including decoded compressed bodies. */
|
|
5
|
+
export declare function fetchGeographyBytes(href: string, maxBytes: number, options: FetchOptions, label: string): Promise<Uint8Array>;
|
|
6
|
+
export {};
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { GeographyError } from "./errors.js";
|
|
2
|
+
import { safeResourceHref } from "./validation.js";
|
|
3
|
+
export function publicDataUrl(href, baseUrl) {
|
|
4
|
+
try {
|
|
5
|
+
const base = new URL(baseUrl);
|
|
6
|
+
if (!safeResourceHref(href) || !["https:", "http:"].includes(base.protocol) || base.username || base.password) {
|
|
7
|
+
throw new Error("unsafe geography URL");
|
|
8
|
+
}
|
|
9
|
+
const url = new URL(href, base);
|
|
10
|
+
if (url.origin !== base.origin)
|
|
11
|
+
throw new Error("off-origin geography URL");
|
|
12
|
+
return url;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
throw new GeographyError("invalid_manifest", "geography URLs must be safe root-relative paths on an HTTP(S) baseUrl");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Shared bounded transport for manifests and geometry, including decoded compressed bodies. */
|
|
19
|
+
export async function fetchGeographyBytes(href, maxBytes, options, label) {
|
|
20
|
+
options.signal?.throwIfAborted();
|
|
21
|
+
const url = publicDataUrl(href, options.baseUrl);
|
|
22
|
+
let response;
|
|
23
|
+
try {
|
|
24
|
+
response = await (options.fetcher ?? fetch)(url, {
|
|
25
|
+
method: "GET",
|
|
26
|
+
credentials: "omit",
|
|
27
|
+
redirect: "error",
|
|
28
|
+
signal: options.signal,
|
|
29
|
+
headers: { accept: "application/geo+json, application/json" },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (options.signal?.aborted)
|
|
34
|
+
throw options.signal.reason ?? error;
|
|
35
|
+
throw new GeographyError("resource_fetch_failed", `${label} could not be fetched`);
|
|
36
|
+
}
|
|
37
|
+
if (options.signal?.aborted) {
|
|
38
|
+
await response.body?.cancel(options.signal.reason).catch(() => undefined);
|
|
39
|
+
options.signal.throwIfAborted();
|
|
40
|
+
}
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
await response.body?.cancel("HTTP error").catch(() => undefined);
|
|
43
|
+
throw new GeographyError("resource_fetch_failed", `${label} returned HTTP ${response.status}`);
|
|
44
|
+
}
|
|
45
|
+
const contentLength = response.headers.get("content-length");
|
|
46
|
+
const contentEncoding = response.headers.get("content-encoding");
|
|
47
|
+
if ((!contentEncoding || contentEncoding === "identity") && contentLength && /^\d+$/.test(contentLength) && Number(contentLength) > maxBytes) {
|
|
48
|
+
await response.body?.cancel("declared size exceeded").catch(() => undefined);
|
|
49
|
+
throw new GeographyError("resource_too_large", `${label} exceeded its byte limit`);
|
|
50
|
+
}
|
|
51
|
+
const reader = response.body?.getReader();
|
|
52
|
+
if (!reader)
|
|
53
|
+
throw new GeographyError("resource_fetch_failed", `${label} has no response body`);
|
|
54
|
+
const chunks = [];
|
|
55
|
+
let size = 0;
|
|
56
|
+
const onAbort = () => { void reader.cancel(options.signal?.reason).catch(() => undefined); };
|
|
57
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
58
|
+
try {
|
|
59
|
+
options.signal?.throwIfAborted();
|
|
60
|
+
while (true) {
|
|
61
|
+
const { done, value } = await reader.read();
|
|
62
|
+
options.signal?.throwIfAborted();
|
|
63
|
+
if (done)
|
|
64
|
+
break;
|
|
65
|
+
if (!value)
|
|
66
|
+
continue;
|
|
67
|
+
size += value.byteLength;
|
|
68
|
+
if (size > maxBytes) {
|
|
69
|
+
await reader.cancel("declared size exceeded").catch(() => undefined);
|
|
70
|
+
throw new GeographyError("resource_too_large", `${label} exceeded its byte limit`);
|
|
71
|
+
}
|
|
72
|
+
chunks.push(value);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (options.signal?.aborted)
|
|
77
|
+
throw options.signal.reason ?? error;
|
|
78
|
+
if (error instanceof GeographyError)
|
|
79
|
+
throw error;
|
|
80
|
+
throw new GeographyError("resource_fetch_failed", `${label} body could not be read`);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
84
|
+
reader.releaseLock();
|
|
85
|
+
}
|
|
86
|
+
const bytes = new Uint8Array(size);
|
|
87
|
+
let offset = 0;
|
|
88
|
+
for (const chunk of chunks) {
|
|
89
|
+
bytes.set(chunk, offset);
|
|
90
|
+
offset += chunk.byteLength;
|
|
91
|
+
}
|
|
92
|
+
return bytes;
|
|
93
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Public entry point for the country-neutral geography contract and helpers. */
|
|
2
|
+
export * from "./types.ts";
|
|
3
|
+
export { GeographyError } from "./errors.ts";
|
|
4
|
+
export { isISODate, isWgs84Position, validateAreaAssertions, validateGeographyManifest, validateGeometry } from "./validation.ts";
|
|
5
|
+
export { loadGeographyResources, makeGeometryRef, selectGeographyResources, sha256Hex, verifyGeographyResource } from "./resources.ts";
|
|
6
|
+
export { summarizeAreaAssertions, validateBoundaryDraft } from "./boundaries.ts";
|
|
7
|
+
export { loadGeographyManifest } from "./manifests.ts";
|
|
8
|
+
export { isWgs84Bounds, intersectsGeographyBounds } from "./spatial.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Public entry point for the country-neutral geography contract and helpers. */
|
|
2
|
+
export * from "./types.js";
|
|
3
|
+
export { GeographyError } from "./errors.js";
|
|
4
|
+
export { isISODate, isWgs84Position, validateAreaAssertions, validateGeographyManifest, validateGeometry } from "./validation.js";
|
|
5
|
+
export { loadGeographyResources, makeGeometryRef, selectGeographyResources, sha256Hex, verifyGeographyResource } from "./resources.js";
|
|
6
|
+
export { summarizeAreaAssertions, validateBoundaryDraft } from "./boundaries.js";
|
|
7
|
+
export { loadGeographyManifest } from "./manifests.js";
|
|
8
|
+
export { isWgs84Bounds, intersectsGeographyBounds } from "./spatial.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { GeographyManifest, LoadManifestOptions } from "./types.ts";
|
|
2
|
+
/** Fetch a public manifest within a byte budget, then validate its provenance and policy. */
|
|
3
|
+
export declare function loadGeographyManifest(href: string, options: LoadManifestOptions): Promise<GeographyManifest>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { GeographyError } from "./errors.js";
|
|
2
|
+
import { fetchGeographyBytes } from "./http.js";
|
|
3
|
+
import { validateGeographyManifest } from "./validation.js";
|
|
4
|
+
/** Fetch a public manifest within a byte budget, then validate its provenance and policy. */
|
|
5
|
+
export async function loadGeographyManifest(href, options) {
|
|
6
|
+
const maxBytes = options.maxBytes ?? 64 * 1024;
|
|
7
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) {
|
|
8
|
+
throw new GeographyError("invalid_manifest", "manifest maxBytes must be a positive safe integer");
|
|
9
|
+
}
|
|
10
|
+
const bytes = await fetchGeographyBytes(href, maxBytes, options, "geography manifest");
|
|
11
|
+
let manifest;
|
|
12
|
+
try {
|
|
13
|
+
manifest = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new GeographyError("invalid_manifest", "geography manifest is not valid UTF-8 JSON");
|
|
17
|
+
}
|
|
18
|
+
const problems = validateGeographyManifest(manifest, options);
|
|
19
|
+
if (problems.length)
|
|
20
|
+
throw new GeographyError("invalid_manifest", problems.slice(0, 8).join("; "));
|
|
21
|
+
return manifest;
|
|
22
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { GeographyManifest, GeographyResource, GeometryResourceData, LoadedResource, LoadResourcesOptions, ResourceSelection } from "./types.ts";
|
|
2
|
+
export declare function selectGeographyResources(manifest: GeographyManifest, request: ResourceSelection): GeographyResource[];
|
|
3
|
+
/** Verify the declared digest, exact decoded size, JSON shape, and feature count. */
|
|
4
|
+
export declare function verifyGeographyResource(resource: GeographyResource, bytes: Uint8Array): Promise<GeometryResourceData>;
|
|
5
|
+
export declare function sha256Hex(bytes: Uint8Array): Promise<string>;
|
|
6
|
+
/** Fetches selected public data without cookies; data resources may not redirect off-origin. */
|
|
7
|
+
export declare function loadGeographyResources(manifest: GeographyManifest, request: ResourceSelection, options: LoadResourcesOptions): Promise<LoadedResource[]>;
|
|
8
|
+
/** Build an opaque, versioned geometry reference suitable for Solon's `areas.geometry_ref`. */
|
|
9
|
+
export declare function makeGeometryRef(input: {
|
|
10
|
+
sourceId: string;
|
|
11
|
+
datasetVersion: string;
|
|
12
|
+
resourceId: string;
|
|
13
|
+
featureId: string | number;
|
|
14
|
+
}): string;
|