@mailwoman/corpus 10.5.0 → 10.7.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/data/PROVENANCE.md +24 -0
- package/data/address-source-register.json +18948 -8757
- package/data/corpus-source-inputs.json +4756 -0
- package/data/license-decisions.json +7260 -0
- package/data/licenses/whosonfirst-licenses.md +3 -2
- package/data/reviewed-ve-postcode-tuples.json +5 -72
- package/data/source-additions.json +2450 -0
- package/data/source-resolutions.json +87 -0
- package/data/training-manifests/v0.7.10-gb-county.effective.json +2335 -0
- package/data/training-manifests/v0.7.10-gb-county.json +1132 -0
- package/data/training-manifests/v0.7.2-address-systems.effective.json +3 -2
- package/data/training-manifests/v0.7.7-no-ppd.json +550 -0
- package/data/training-manifests/v0.7.8-rights-clean.json +538 -0
- package/data/training-manifests/v0.7.9-country-gap.json +1132 -0
- package/data/us-situs-sources.json +128 -0
- package/lib/adapters/cordis/adapter.ts +1 -1
- package/lib/adapters/libpostal-dictionary.ts +23 -3
- package/lib/adapters/osm/adapter.ts +42 -11
- package/lib/adapters.ts +14 -0
- package/lib/az/adapters/opendata/adapter.ts +389 -0
- package/lib/bs/adapters/gbpa/adapter.ts +248 -0
- package/lib/build/types.ts +16 -0
- package/lib/build.ts +21 -0
- package/lib/ee/adapters/ads/adapter.ts +18 -0
- package/lib/fo/adapters/adressur/adapter.ts +242 -0
- package/lib/gb/adapters/os-open/adapter.ts +14 -2
- package/lib/gb/county.ts +59 -0
- package/lib/gb/os-open/{open-names.ts → open/names.ts} +41 -0
- package/lib/gu/adapters/bsp/adapter.ts +434 -0
- package/lib/im/adapters/land-registry/adapter.ts +340 -0
- package/lib/international/recipes/locale.ts +164 -19
- package/lib/li/adapters/llv/adapter.ts +246 -0
- package/lib/recipes/bare/postcode.ts +10 -34
- package/lib/recipes/country-balanced.ts +95 -78
- package/lib/recipes/intersection.ts +22 -30
- package/lib/recipes/po/box/cedex/recipe.ts +8 -5
- package/lib/recipes/po/box/cedex/sources.ts +2 -23
- package/lib/recipes/scaffold.ts +28 -3
- package/lib/recipes/sources.ts +112 -1
- package/lib/recipes/street/affix.ts +17 -29
- package/lib/recipes/sub/venue/context.ts +33 -9
- package/lib/recipes/sub/venue/sources.ts +54 -13
- package/lib/recipes/sub/venue.ts +68 -23
- package/lib/recipes/unit.ts +20 -7
- package/lib/registers.ts +33 -0
- package/lib/source-register/corpus-inputs.ts +528 -0
- package/lib/source-register/effective-manifest.ts +37 -5
- package/lib/source-register/types.ts +20 -0
- package/lib/south-asia/recipes/register.ts +1 -1
- package/lib/tools/overlay/manifest.ts +109 -1
- package/lib/tools/source-register/build.ts +47 -1
- package/lib/tools/variety-sample.ts +633 -0
- package/lib/us/adapters/usgov/nad/adapter.ts +39 -10
- package/lib/us/recipes/skeleton-sources.ts +156 -0
- package/lib/us/tools/nad-skeletons.ts +395 -0
- package/lib/uy/adapters/ide/adapter.ts +275 -0
- package/lib/ve/recipes/reviewed-postcode-tail.ts +29 -6
- package/out/adapters/cordis/adapter.d.ts +1 -1
- package/out/adapters/cordis/adapter.js +1 -1
- package/out/adapters/libpostal-dictionary.d.ts +9 -0
- package/out/adapters/libpostal-dictionary.d.ts.map +1 -1
- package/out/adapters/libpostal-dictionary.js +19 -3
- package/out/adapters/libpostal-dictionary.js.map +1 -1
- package/out/adapters/osm/adapter.d.ts +12 -1
- package/out/adapters/osm/adapter.d.ts.map +1 -1
- package/out/adapters/osm/adapter.js +30 -13
- package/out/adapters/osm/adapter.js.map +1 -1
- package/out/adapters.d.ts.map +1 -1
- package/out/adapters.js +14 -0
- package/out/adapters.js.map +1 -1
- package/out/az/adapters/opendata/adapter.d.ts +88 -0
- package/out/az/adapters/opendata/adapter.d.ts.map +1 -0
- package/out/az/adapters/opendata/adapter.js +282 -0
- package/out/az/adapters/opendata/adapter.js.map +1 -0
- package/out/bs/adapters/gbpa/adapter.d.ts +64 -0
- package/out/bs/adapters/gbpa/adapter.d.ts.map +1 -0
- package/out/bs/adapters/gbpa/adapter.js +187 -0
- package/out/bs/adapters/gbpa/adapter.js.map +1 -0
- package/out/build/types.d.ts +15 -0
- package/out/build/types.d.ts.map +1 -1
- package/out/build/types.js.map +1 -1
- package/out/build.d.ts.map +1 -1
- package/out/build.js +17 -0
- package/out/build.js.map +1 -1
- package/out/ee/adapters/ads/adapter.d.ts +6 -0
- package/out/ee/adapters/ads/adapter.d.ts.map +1 -1
- package/out/ee/adapters/ads/adapter.js +16 -0
- package/out/ee/adapters/ads/adapter.js.map +1 -1
- package/out/fo/adapters/adressur/adapter.d.ts +63 -0
- package/out/fo/adapters/adressur/adapter.d.ts.map +1 -0
- package/out/fo/adapters/adressur/adapter.js +177 -0
- package/out/fo/adapters/adressur/adapter.js.map +1 -0
- package/out/gb/adapters/os-open/adapter.d.ts +10 -2
- package/out/gb/adapters/os-open/adapter.d.ts.map +1 -1
- package/out/gb/adapters/os-open/adapter.js +13 -2
- package/out/gb/adapters/os-open/adapter.js.map +1 -1
- package/out/gb/county.d.ts +30 -0
- package/out/gb/county.d.ts.map +1 -0
- package/out/gb/county.js +51 -0
- package/out/gb/county.js.map +1 -0
- package/out/gb/os-open/{open-names.d.ts → open/names.d.ts} +18 -1
- package/out/gb/os-open/open/names.d.ts.map +1 -0
- package/out/gb/os-open/{open-names.js → open/names.js} +20 -1
- package/out/gb/os-open/open/names.js.map +1 -0
- package/out/gb/os-open/{open-roads.d.ts → open/roads.d.ts} +1 -1
- package/out/gb/os-open/open/roads.d.ts.map +1 -0
- package/out/gb/os-open/{open-roads.js → open/roads.js} +1 -1
- package/out/gb/os-open/open/roads.js.map +1 -0
- package/out/gu/adapters/bsp/adapter.d.ts +83 -0
- package/out/gu/adapters/bsp/adapter.d.ts.map +1 -0
- package/out/gu/adapters/bsp/adapter.js +346 -0
- package/out/gu/adapters/bsp/adapter.js.map +1 -0
- package/out/im/adapters/land-registry/adapter.d.ts +68 -0
- package/out/im/adapters/land-registry/adapter.d.ts.map +1 -0
- package/out/im/adapters/land-registry/adapter.js +261 -0
- package/out/im/adapters/land-registry/adapter.js.map +1 -0
- package/out/international/recipes/locale.d.ts +24 -4
- package/out/international/recipes/locale.d.ts.map +1 -1
- package/out/international/recipes/locale.js +107 -15
- package/out/international/recipes/locale.js.map +1 -1
- package/out/li/adapters/llv/adapter.d.ts +69 -0
- package/out/li/adapters/llv/adapter.d.ts.map +1 -0
- package/out/li/adapters/llv/adapter.js +181 -0
- package/out/li/adapters/llv/adapter.js.map +1 -0
- package/out/recipes/bare/postcode.d.ts.map +1 -1
- package/out/recipes/bare/postcode.js +9 -30
- package/out/recipes/bare/postcode.js.map +1 -1
- package/out/recipes/country-balanced.d.ts.map +1 -1
- package/out/recipes/country-balanced.js +82 -73
- package/out/recipes/country-balanced.js.map +1 -1
- package/out/recipes/intersection.d.ts +3 -2
- package/out/recipes/intersection.d.ts.map +1 -1
- package/out/recipes/intersection.js +20 -26
- package/out/recipes/intersection.js.map +1 -1
- package/out/recipes/po/box/cedex/recipe.d.ts.map +1 -1
- package/out/recipes/po/box/cedex/recipe.js +5 -4
- package/out/recipes/po/box/cedex/recipe.js.map +1 -1
- package/out/recipes/po/box/cedex/sources.d.ts +2 -45
- package/out/recipes/po/box/cedex/sources.d.ts.map +1 -1
- package/out/recipes/po/box/cedex/sources.js +0 -20
- package/out/recipes/po/box/cedex/sources.js.map +1 -1
- package/out/recipes/scaffold.d.ts +15 -2
- package/out/recipes/scaffold.d.ts.map +1 -1
- package/out/recipes/scaffold.js +21 -1
- package/out/recipes/scaffold.js.map +1 -1
- package/out/recipes/sources.d.ts.map +1 -1
- package/out/recipes/sources.js +112 -1
- package/out/recipes/sources.js.map +1 -1
- package/out/recipes/street/affix.d.ts.map +1 -1
- package/out/recipes/street/affix.js +8 -20
- package/out/recipes/street/affix.js.map +1 -1
- package/out/recipes/sub/venue/context.d.ts +14 -3
- package/out/recipes/sub/venue/context.d.ts.map +1 -1
- package/out/recipes/sub/venue/context.js +29 -9
- package/out/recipes/sub/venue/context.js.map +1 -1
- package/out/recipes/sub/venue/sources.d.ts +41 -8
- package/out/recipes/sub/venue/sources.d.ts.map +1 -1
- package/out/recipes/sub/venue/sources.js +26 -7
- package/out/recipes/sub/venue/sources.js.map +1 -1
- package/out/recipes/sub/venue.d.ts.map +1 -1
- package/out/recipes/sub/venue.js +35 -14
- package/out/recipes/sub/venue.js.map +1 -1
- package/out/recipes/unit.d.ts +4 -0
- package/out/recipes/unit.d.ts.map +1 -1
- package/out/recipes/unit.js +7 -6
- package/out/recipes/unit.js.map +1 -1
- package/out/registers.d.ts +33 -0
- package/out/registers.d.ts.map +1 -1
- package/out/registers.js +33 -0
- package/out/registers.js.map +1 -1
- package/out/source-register/corpus-inputs.d.ts +240 -0
- package/out/source-register/corpus-inputs.d.ts.map +1 -0
- package/out/source-register/corpus-inputs.js +275 -0
- package/out/source-register/corpus-inputs.js.map +1 -0
- package/out/source-register/effective-manifest.d.ts +2 -0
- package/out/source-register/effective-manifest.d.ts.map +1 -1
- package/out/source-register/effective-manifest.js +33 -4
- package/out/source-register/effective-manifest.js.map +1 -1
- package/out/source-register/types.d.ts +21 -1
- package/out/source-register/types.d.ts.map +1 -1
- package/out/source-register/types.js +20 -0
- package/out/source-register/types.js.map +1 -1
- package/out/south-asia/recipes/register.js +1 -1
- package/out/south-asia/recipes/register.js.map +1 -1
- package/out/tools/overlay/manifest.d.ts +7 -0
- package/out/tools/overlay/manifest.d.ts.map +1 -1
- package/out/tools/overlay/manifest.js +72 -1
- package/out/tools/overlay/manifest.js.map +1 -1
- package/out/tools/source-register/build.d.ts +9 -0
- package/out/tools/source-register/build.d.ts.map +1 -1
- package/out/tools/source-register/build.js +25 -3
- package/out/tools/source-register/build.js.map +1 -1
- package/out/tools/variety-sample.d.ts +196 -0
- package/out/tools/variety-sample.d.ts.map +1 -0
- package/out/tools/variety-sample.js +417 -0
- package/out/tools/variety-sample.js.map +1 -0
- package/out/us/adapters/usgov/nad/adapter.d.ts +80 -0
- package/out/us/adapters/usgov/nad/adapter.d.ts.map +1 -1
- package/out/us/adapters/usgov/nad/adapter.js +27 -8
- package/out/us/adapters/usgov/nad/adapter.js.map +1 -1
- package/out/us/recipes/skeleton-sources.d.ts +92 -0
- package/out/us/recipes/skeleton-sources.d.ts.map +1 -0
- package/out/us/recipes/skeleton-sources.js +121 -0
- package/out/us/recipes/skeleton-sources.js.map +1 -0
- package/out/us/tools/nad-skeletons.d.ts +120 -0
- package/out/us/tools/nad-skeletons.d.ts.map +1 -0
- package/out/us/tools/nad-skeletons.js +262 -0
- package/out/us/tools/nad-skeletons.js.map +1 -0
- package/out/uy/adapters/ide/adapter.d.ts +64 -0
- package/out/uy/adapters/ide/adapter.d.ts.map +1 -0
- package/out/uy/adapters/ide/adapter.js +206 -0
- package/out/uy/adapters/ide/adapter.js.map +1 -0
- package/out/ve/recipes/reviewed-postcode-tail.d.ts +6 -2
- package/out/ve/recipes/reviewed-postcode-tail.d.ts.map +1 -1
- package/out/ve/recipes/reviewed-postcode-tail.js +16 -5
- package/out/ve/recipes/reviewed-postcode-tail.js.map +1 -1
- package/package.json +14 -14
- package/lib/recipes/unit/sources.ts +0 -44
- package/lib/tools/fetch/ppd.ts +0 -121
- package/out/gb/os-open/open-names.d.ts.map +0 -1
- package/out/gb/os-open/open-names.js.map +0 -1
- package/out/gb/os-open/open-roads.d.ts.map +0 -1
- package/out/gb/os-open/open-roads.js.map +0 -1
- package/out/recipes/unit/sources.d.ts +0 -28
- package/out/recipes/unit/sources.d.ts.map +0 -1
- package/out/recipes/unit/sources.js +0 -30
- package/out/recipes/unit/sources.js.map +0 -1
- package/out/tools/fetch/ppd.d.ts +0 -15
- package/out/tools/fetch/ppd.d.ts.map +0 -1
- package/out/tools/fetch/ppd.js +0 -86
- package/out/tools/fetch/ppd.js.map +0 -1
- /package/lib/gb/os-open/{open-roads.ts → open/roads.ts} +0 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Joins a corpus source id to the register sources whose data its rows are built from.
|
|
7
|
+
*
|
|
8
|
+
* A corpus source id is the `source` value stored on every corpus row, keyed by a training config's
|
|
9
|
+
* `source_weights` and listed in a training manifest. The register scopes a source to one publisher in one
|
|
10
|
+
* jurisdiction and records its license decision. This table is the one record joining the two. Before it, a source whose
|
|
11
|
+
* register row was refused or never read could feed a corpus build and a training run: HM Land Registry Price Paid
|
|
12
|
+
* Data reached `rendered-gb` and `spliced-sub-venue` while its row read `unchecked` (#2635).
|
|
13
|
+
*
|
|
14
|
+
* The table is `packages/corpus/data/corpus-source-inputs.json`. A derived or synthetic source lists every input
|
|
15
|
+
* whose data reaches one of its rows. An input with no register row gets one in `source-additions.json`, under an
|
|
16
|
+
* `unchecked` decision, so the gap reads as a gap rather than as an absence.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readLocalJSONFile } from "@mailwoman/core/fs/readers"
|
|
20
|
+
import { stringifyJSON } from "@mailwoman/core/json"
|
|
21
|
+
import { resolveModulePath } from "@mailwoman/core/module/resolvers"
|
|
22
|
+
import type { PathBuilderLike } from "path-ts"
|
|
23
|
+
|
|
24
|
+
import { permissionFor, readAddressSourceRegister } from "#source-register/register"
|
|
25
|
+
import {
|
|
26
|
+
LicenseReviewState,
|
|
27
|
+
OperationPermission,
|
|
28
|
+
SourceOperation,
|
|
29
|
+
type AddressSourceRegister,
|
|
30
|
+
type LicenseDecision,
|
|
31
|
+
} from "#source-register/types"
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The operations a register decision must permit before a source's rows may
|
|
35
|
+
* be built into a corpus or trained on.
|
|
36
|
+
*
|
|
37
|
+
* Both need adaptation: normalizing an address into labeled components is a derived work,
|
|
38
|
+
* and fitting a model on it is another.
|
|
39
|
+
*/
|
|
40
|
+
export const TRAINING_OPERATIONS: readonly SourceOperation[] = [SourceOperation.Transform, SourceOperation.Train]
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* One register source whose data reaches a corpus source's rows.
|
|
44
|
+
*/
|
|
45
|
+
export interface CorpusSourceInput {
|
|
46
|
+
/**
|
|
47
|
+
* The register's `sourceID`.
|
|
48
|
+
*/
|
|
49
|
+
registerSource: string
|
|
50
|
+
/**
|
|
51
|
+
* The `licenseID` that register source points at, repeated so a reviewer reads the decision key here.
|
|
52
|
+
*
|
|
53
|
+
* {@linkcode auditCorpusSourceInputs} refuses a value that disagrees with the register.
|
|
54
|
+
*/
|
|
55
|
+
license: string
|
|
56
|
+
/**
|
|
57
|
+
* What the corpus source reads from it: the file, the fields and the component they fill.
|
|
58
|
+
*/
|
|
59
|
+
reads: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Everything one corpus source's rows are built from.
|
|
64
|
+
*/
|
|
65
|
+
export interface CorpusSourceMapping {
|
|
66
|
+
/**
|
|
67
|
+
* Other spellings rows of this source are stored under, such as the retired `synth-*` spelling.
|
|
68
|
+
*/
|
|
69
|
+
aliases?: readonly string[]
|
|
70
|
+
/**
|
|
71
|
+
* Every register source whose data reaches a row.
|
|
72
|
+
*/
|
|
73
|
+
inputs: readonly CorpusSourceInput[]
|
|
74
|
+
/**
|
|
75
|
+
* Inputs written in this repository, which need no register row: a hand-written word list, a template.
|
|
76
|
+
*/
|
|
77
|
+
authored?: readonly string[]
|
|
78
|
+
note?: string
|
|
79
|
+
/**
|
|
80
|
+
* What the source read in corpora built before its producer changed inputs under the same id.
|
|
81
|
+
*
|
|
82
|
+
* `inputs` describes what a build reads today.
|
|
83
|
+
* The build and training checks read it.
|
|
84
|
+
*
|
|
85
|
+
* A corpus already built keeps the rows it was built from, so the release audit reads the
|
|
86
|
+
* entry naming that corpus: `spliced-sub-venue` kept its name when its GB skeletons moved from
|
|
87
|
+
* Price Paid Data to OS OpenData, and the published model trained on the Price Paid Data rows.
|
|
88
|
+
*/
|
|
89
|
+
history?: readonly CorpusSourceHistory[]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The inputs one source read in the corpora named.
|
|
94
|
+
*/
|
|
95
|
+
export interface CorpusSourceHistory {
|
|
96
|
+
corpora: readonly string[]
|
|
97
|
+
inputs: readonly CorpusSourceInput[]
|
|
98
|
+
because: string
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The committed table.
|
|
103
|
+
*/
|
|
104
|
+
export interface CorpusSourceInputsFile {
|
|
105
|
+
sources: Readonly<Record<string, CorpusSourceMapping>>
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The table indexed for lookup under every spelling it records.
|
|
110
|
+
*/
|
|
111
|
+
export interface CorpusSourceInputs {
|
|
112
|
+
readonly file: CorpusSourceInputsFile
|
|
113
|
+
/**
|
|
114
|
+
* The mapping a spelling resolves to, with the id it is filed under.
|
|
115
|
+
*/
|
|
116
|
+
resolve(source: string): { id: string; mapping: CorpusSourceMapping } | null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Resolves the committed table through the package's `./data/*` export.
|
|
121
|
+
*/
|
|
122
|
+
export function corpusSourceInputsPath(): string {
|
|
123
|
+
return resolveModulePath("@mailwoman/corpus/data/corpus-source-inputs.json")
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Indexes a table by id and alias.
|
|
128
|
+
*
|
|
129
|
+
* @throws When two entries claim one spelling, because a lookup would then answer for whichever came last.
|
|
130
|
+
*/
|
|
131
|
+
export function indexCorpusSourceInputs(file: CorpusSourceInputsFile): CorpusSourceInputs {
|
|
132
|
+
const bySpelling = new Map<string, string>()
|
|
133
|
+
|
|
134
|
+
for (const [id, mapping] of Object.entries(file.sources)) {
|
|
135
|
+
for (const spelling of [id, ...(mapping.aliases ?? [])]) {
|
|
136
|
+
const claimed = bySpelling.get(spelling)
|
|
137
|
+
|
|
138
|
+
if (claimed && claimed !== id) {
|
|
139
|
+
throw new Error(`corpus source spelling ${stringifyJSON(spelling)} is claimed by both ${claimed} and ${id}`)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
bySpelling.set(spelling, id)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
file,
|
|
148
|
+
resolve(source) {
|
|
149
|
+
const id = bySpelling.get(source)
|
|
150
|
+
|
|
151
|
+
return id ? { id, mapping: file.sources[id]! } : null
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* One message per structural problem in the table, read against the register.
|
|
158
|
+
*/
|
|
159
|
+
export function auditCorpusSourceInputs(file: CorpusSourceInputsFile, register: AddressSourceRegister): string[] {
|
|
160
|
+
const problems: string[] = []
|
|
161
|
+
const sources = new Map(register.sources.map((source) => [source.sourceID, source]))
|
|
162
|
+
|
|
163
|
+
for (const [id, mapping] of Object.entries(file.sources)) {
|
|
164
|
+
const named = stringifyJSON(id)
|
|
165
|
+
|
|
166
|
+
if (!mapping.inputs.length && !mapping.authored?.length) {
|
|
167
|
+
problems.push(`corpus source ${named} lists no input, so nothing records what its rows are built from`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const entry of mapping.history ?? []) {
|
|
171
|
+
if (!entry.corpora.length || !entry.because) {
|
|
172
|
+
problems.push(`corpus source ${named} has a history entry naming no corpus or giving no reason`)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const input of [...mapping.inputs, ...(mapping.history ?? []).flatMap((entry) => entry.inputs)]) {
|
|
177
|
+
const row = sources.get(input.registerSource)
|
|
178
|
+
|
|
179
|
+
if (!row) {
|
|
180
|
+
problems.push(
|
|
181
|
+
`corpus source ${named} reads register source ${stringifyJSON(input.registerSource)}, which the register ` +
|
|
182
|
+
"does not carry. Add the row to `source-additions.json` and regenerate the register."
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (row.license !== input.license) {
|
|
189
|
+
problems.push(
|
|
190
|
+
`corpus source ${named} records license ${stringifyJSON(input.license)} for ${input.registerSource}, ` +
|
|
191
|
+
`and the register points that source at ${stringifyJSON(row.license)}`
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!input.reads) {
|
|
196
|
+
problems.push(`corpus source ${named} reads ${input.registerSource} and does not say what it reads`)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return problems
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Reads the committed table and checks it against the register.
|
|
206
|
+
*
|
|
207
|
+
* @throws When the file does not parse or the audit reports a problem.
|
|
208
|
+
*/
|
|
209
|
+
export async function readCorpusSourceInputs(
|
|
210
|
+
register: AddressSourceRegister,
|
|
211
|
+
path?: PathBuilderLike
|
|
212
|
+
): Promise<CorpusSourceInputs> {
|
|
213
|
+
const resolved = path ?? corpusSourceInputsPath()
|
|
214
|
+
const file = await readLocalJSONFile<CorpusSourceInputsFile>(resolved)
|
|
215
|
+
const problems = auditCorpusSourceInputs(file, register)
|
|
216
|
+
|
|
217
|
+
if (problems.length) {
|
|
218
|
+
throw new Error(`${resolved} failed its audit against the register:\n ${problems.join("\n ")}`)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return indexCorpusSourceInputs(file)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Why one corpus source lacks the permission {@linkcode TRAINING_OPERATIONS} need.
|
|
226
|
+
*/
|
|
227
|
+
export interface SourceRightsGap {
|
|
228
|
+
/**
|
|
229
|
+
* The corpus source id as the caller named it.
|
|
230
|
+
*/
|
|
231
|
+
source: string
|
|
232
|
+
/**
|
|
233
|
+
* The register source lacking the permission, or `null` when the table has no entry for the corpus source.
|
|
234
|
+
*/
|
|
235
|
+
registerSource: string | null
|
|
236
|
+
licenseID: string | null
|
|
237
|
+
/**
|
|
238
|
+
* The decision's review state, or `null` when there is no decision to read.
|
|
239
|
+
*/
|
|
240
|
+
state: LicenseDecision["state"] | null
|
|
241
|
+
/**
|
|
242
|
+
* The operations the decision does not permit.
|
|
243
|
+
*/
|
|
244
|
+
missing: SourceOperation[]
|
|
245
|
+
because: string
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Every reason a corpus source's rows may not be built into a corpus or trained on.
|
|
250
|
+
*
|
|
251
|
+
* An empty array is the only answer a caller may read as permission.
|
|
252
|
+
* A source the table does not map answers one gap whose `registerSource` is `null`,
|
|
253
|
+
* because an unmapped source is one nobody has traced to its publishers.
|
|
254
|
+
*
|
|
255
|
+
* `corpusVersion` names a corpus already built, so a source whose inputs changed under
|
|
256
|
+
* the same id is read against what that corpus's rows were built from.
|
|
257
|
+
* A build or training check omits it and reads today's inputs.
|
|
258
|
+
*/
|
|
259
|
+
export function sourceRightsGaps(
|
|
260
|
+
source: string,
|
|
261
|
+
inputs: CorpusSourceInputs,
|
|
262
|
+
register: AddressSourceRegister,
|
|
263
|
+
corpusVersion?: string
|
|
264
|
+
): SourceRightsGap[] {
|
|
265
|
+
const resolved = inputs.resolve(source)
|
|
266
|
+
|
|
267
|
+
if (!resolved) {
|
|
268
|
+
return [
|
|
269
|
+
{
|
|
270
|
+
source,
|
|
271
|
+
registerSource: null,
|
|
272
|
+
licenseID: null,
|
|
273
|
+
state: null,
|
|
274
|
+
missing: [...TRAINING_OPERATIONS],
|
|
275
|
+
because:
|
|
276
|
+
"no entry in packages/corpus/data/corpus-source-inputs.json says which register sources its rows are " +
|
|
277
|
+
"built from",
|
|
278
|
+
},
|
|
279
|
+
]
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const decisions = new Map(register.licenses.map((decision) => [decision.licenseID, decision]))
|
|
283
|
+
const rows = new Map(register.sources.map((row) => [row.sourceID, row]))
|
|
284
|
+
const gaps: SourceRightsGap[] = []
|
|
285
|
+
|
|
286
|
+
const former = corpusVersion
|
|
287
|
+
? resolved.mapping.history?.find((entry) => entry.corpora.includes(corpusVersion))
|
|
288
|
+
: undefined
|
|
289
|
+
|
|
290
|
+
for (const input of former?.inputs ?? resolved.mapping.inputs) {
|
|
291
|
+
const row = rows.get(input.registerSource)
|
|
292
|
+
const decision = row ? decisions.get(row.license) : undefined
|
|
293
|
+
|
|
294
|
+
if (!row || !decision) {
|
|
295
|
+
gaps.push({
|
|
296
|
+
source,
|
|
297
|
+
registerSource: input.registerSource,
|
|
298
|
+
licenseID: row?.license ?? null,
|
|
299
|
+
state: null,
|
|
300
|
+
missing: [...TRAINING_OPERATIONS],
|
|
301
|
+
because: row
|
|
302
|
+
? `the register declares no decision ${stringifyJSON(row.license)}`
|
|
303
|
+
: "the register has no such row",
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
continue
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const readings = TRAINING_OPERATIONS.map((operation) => ({
|
|
310
|
+
operation,
|
|
311
|
+
reading: permissionFor(decision, operation),
|
|
312
|
+
}))
|
|
313
|
+
|
|
314
|
+
const missing = readings.filter(({ reading }) => reading.permission !== OperationPermission.Permitted)
|
|
315
|
+
|
|
316
|
+
if (!missing.length) continue
|
|
317
|
+
|
|
318
|
+
gaps.push({
|
|
319
|
+
source,
|
|
320
|
+
registerSource: row.sourceID,
|
|
321
|
+
licenseID: decision.licenseID,
|
|
322
|
+
state: decision.state,
|
|
323
|
+
missing: missing.map(({ operation }) => operation),
|
|
324
|
+
because:
|
|
325
|
+
decision.state === LicenseReviewState.Refused
|
|
326
|
+
? `the decision is refused: ${decision.refusedBecause}`
|
|
327
|
+
: missing[0]!.reading.because,
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return gaps
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* One line naming the corpus source, the register row and the missing permission.
|
|
336
|
+
*/
|
|
337
|
+
export function describeSourceRightsGap(gap: SourceRightsGap): string {
|
|
338
|
+
const missing = gap.missing.join(" and ")
|
|
339
|
+
|
|
340
|
+
if (!gap.registerSource) {
|
|
341
|
+
return `corpus source ${gap.source} is unmapped and so lacks a permission for ${missing}: ${gap.because}`
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
`corpus source ${gap.source} reads register source ${gap.registerSource} ` +
|
|
346
|
+
`(license ${gap.licenseID ?? "none"}, ${gap.state ?? "no decision"}), which does not permit ${missing}: ${gap.because}`
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Raised when a build or training run would admit a source lacking the permission.
|
|
352
|
+
*/
|
|
353
|
+
export class SourceRightsError extends Error {
|
|
354
|
+
readonly gaps: readonly SourceRightsGap[]
|
|
355
|
+
|
|
356
|
+
constructor(context: string, gaps: readonly SourceRightsGap[]) {
|
|
357
|
+
super(
|
|
358
|
+
`${context} refuses ${new Set(gaps.map((gap) => gap.source)).size} corpus source(s) whose register rows ` +
|
|
359
|
+
`lack an elected decision permitting ${TRAINING_OPERATIONS.join(" and ")}:\n ` +
|
|
360
|
+
gaps.map(describeSourceRightsGap).join("\n ") +
|
|
361
|
+
"\nRecord the decision in packages/corpus/data/license-decisions.json, map the source in " +
|
|
362
|
+
"packages/corpus/data/corpus-source-inputs.json, or pass --unshippable-reason <why> for a local " +
|
|
363
|
+
"experiment, which marks the output as not shippable."
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
this.name = "SourceRightsError"
|
|
367
|
+
this.gaps = gaps
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* What a checked output records about the sources it admitted.
|
|
373
|
+
*
|
|
374
|
+
* Its field names are snake_case because it is written into corpus manifests,
|
|
375
|
+
* which a Python reader also reads.
|
|
376
|
+
*/
|
|
377
|
+
export interface SourceRightsRecord {
|
|
378
|
+
/**
|
|
379
|
+
* False when an override admitted a source lacking the permission.
|
|
380
|
+
*/
|
|
381
|
+
shippable: boolean
|
|
382
|
+
unshippable_reason: string | null
|
|
383
|
+
/**
|
|
384
|
+
* Every corpus source the check read, sorted.
|
|
385
|
+
*/
|
|
386
|
+
sources_checked: string[]
|
|
387
|
+
gaps: SourceRightsGap[]
|
|
388
|
+
/**
|
|
389
|
+
* Inputs whose corpus sources could not be read, so the check could not judge them,
|
|
390
|
+
* such as a base corpus whose files name no source and whose manifest carries no record.
|
|
391
|
+
*/
|
|
392
|
+
unverified: string[]
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Checks corpus sources one at a time as an output admits them.
|
|
397
|
+
*/
|
|
398
|
+
export interface SourceRightsAdmission {
|
|
399
|
+
/**
|
|
400
|
+
* Admits a source or throws {@linkcode SourceRightsError}.
|
|
401
|
+
*
|
|
402
|
+
* Under an override it logs each gap once and returns them instead.
|
|
403
|
+
*/
|
|
404
|
+
admit(source: string): readonly SourceRightsGap[]
|
|
405
|
+
/**
|
|
406
|
+
* Admits several sources at once, so a refusal names every one of them.
|
|
407
|
+
*/
|
|
408
|
+
admitAll(sources: Iterable<string>): readonly SourceRightsGap[]
|
|
409
|
+
/**
|
|
410
|
+
* Refuses an input whose sources cannot be read, or records it under an override.
|
|
411
|
+
*
|
|
412
|
+
* An input the check cannot read stays unverified rather than clear.
|
|
413
|
+
*/
|
|
414
|
+
admitUnverified(description: string): void
|
|
415
|
+
record(): SourceRightsRecord
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Options for {@linkcode createSourceRightsAdmission}.
|
|
420
|
+
*/
|
|
421
|
+
export interface SourceRightsAdmissionOptions {
|
|
422
|
+
/**
|
|
423
|
+
* Names the output in a refusal, as `corpus build v0.7.7` or `recipe sub-venue`.
|
|
424
|
+
*/
|
|
425
|
+
context: string
|
|
426
|
+
/**
|
|
427
|
+
* Why a local experiment admits sources lacking the permission.
|
|
428
|
+
*
|
|
429
|
+
* Setting it marks the output unshippable rather than relaxing the rule.
|
|
430
|
+
*/
|
|
431
|
+
unshippableReason?: string | null
|
|
432
|
+
log?: (line: string) => void
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Builds the admission check over an already-read register and table.
|
|
437
|
+
*
|
|
438
|
+
* @throws When `unshippableReason` is set and blank, because an override must say why.
|
|
439
|
+
*/
|
|
440
|
+
export function createSourceRightsAdmission(
|
|
441
|
+
register: AddressSourceRegister,
|
|
442
|
+
inputs: CorpusSourceInputs,
|
|
443
|
+
options: SourceRightsAdmissionOptions
|
|
444
|
+
): SourceRightsAdmission {
|
|
445
|
+
const reason = options.unshippableReason ?? null
|
|
446
|
+
|
|
447
|
+
if (reason !== null && !reason.trim()) {
|
|
448
|
+
throw new Error(`${options.context}: --unshippable-reason must say why the output is not shippable`)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const log = options.log ?? ((line: string) => console.error(line))
|
|
452
|
+
const checked = new Map<string, readonly SourceRightsGap[]>()
|
|
453
|
+
|
|
454
|
+
const admitAll = (sources: Iterable<string>): readonly SourceRightsGap[] => {
|
|
455
|
+
const fresh: SourceRightsGap[] = []
|
|
456
|
+
const all: SourceRightsGap[] = []
|
|
457
|
+
|
|
458
|
+
for (const source of sources) {
|
|
459
|
+
const known = checked.get(source)
|
|
460
|
+
|
|
461
|
+
if (known) {
|
|
462
|
+
all.push(...known)
|
|
463
|
+
|
|
464
|
+
continue
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const gaps = sourceRightsGaps(source, inputs, register)
|
|
468
|
+
|
|
469
|
+
fresh.push(...gaps)
|
|
470
|
+
all.push(...gaps)
|
|
471
|
+
|
|
472
|
+
if (!gaps.length || reason !== null) {
|
|
473
|
+
checked.set(source, gaps)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (!fresh.length) return all
|
|
478
|
+
|
|
479
|
+
if (reason === null) throw new SourceRightsError(options.context, fresh)
|
|
480
|
+
|
|
481
|
+
for (const gap of fresh) {
|
|
482
|
+
log(`UNSHIPPABLE ${options.context}: ${describeSourceRightsGap(gap)} (override: ${reason})`)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return all
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const unverified: string[] = []
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
admit: (source) => admitAll([source]),
|
|
492
|
+
admitAll,
|
|
493
|
+
admitUnverified: (description) => {
|
|
494
|
+
if (reason === null) {
|
|
495
|
+
throw new Error(
|
|
496
|
+
`${options.context} cannot check the corpus sources of ${description}, and an input it cannot read is ` +
|
|
497
|
+
"not one it found clear. Pass --unshippable-reason <why> for a local experiment, which marks the " +
|
|
498
|
+
"output as not shippable."
|
|
499
|
+
)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
log(`UNSHIPPABLE ${options.context}: the corpus sources of ${description} were not checked (override: ${reason})`)
|
|
503
|
+
unverified.push(description)
|
|
504
|
+
},
|
|
505
|
+
record: () => {
|
|
506
|
+
const gaps = [...checked.values()].flat()
|
|
507
|
+
const shippable = gaps.length === 0 && unverified.length === 0
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
shippable,
|
|
511
|
+
unshippable_reason: shippable ? null : reason,
|
|
512
|
+
sources_checked: [...checked.keys()].toSorted(),
|
|
513
|
+
gaps,
|
|
514
|
+
unverified: [...unverified],
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Reads the committed register and table and builds the admission check over them.
|
|
522
|
+
*/
|
|
523
|
+
export async function openSourceRightsAdmission(options: SourceRightsAdmissionOptions): Promise<SourceRightsAdmission> {
|
|
524
|
+
const register = await readAddressSourceRegister()
|
|
525
|
+
const inputs = await readCorpusSourceInputs(register)
|
|
526
|
+
|
|
527
|
+
return createSourceRightsAdmission(register, inputs, options)
|
|
528
|
+
}
|
|
@@ -28,6 +28,7 @@ import { LicenseObligation, summarizeLicense } from "@mailwoman/core/license/obl
|
|
|
28
28
|
import type { PathBuilder } from "path-ts"
|
|
29
29
|
|
|
30
30
|
import { OSM_ATTRIBUTION, OSM_LICENSE } from "#adapters/osm/adapter"
|
|
31
|
+
import { currentSourceName } from "#recipes/sources"
|
|
31
32
|
import type { TrainingManifest } from "#source-register/training-manifest"
|
|
32
33
|
|
|
33
34
|
/**
|
|
@@ -178,12 +179,45 @@ const ATTRIBUTION_STATEMENTS: Readonly<Record<string, string>> = {
|
|
|
178
179
|
[OSM_LICENSE]: OSM_ATTRIBUTION,
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Recognized licenses carried by contents of a source whose rows state one free-text
|
|
184
|
+
* license, keyed by the source's current spelling.
|
|
185
|
+
*
|
|
186
|
+
* The derivation groups sources by the license their rows state.
|
|
187
|
+
* The `sub-venue` recipe states one license over three publications.
|
|
188
|
+
*
|
|
189
|
+
* That license is free text, and the OpenStreetMap venue names it carries are under ODbL 1.0.
|
|
190
|
+
* Grouping by that text alone leaves the source out of the ODbL group,
|
|
191
|
+
* the group that carries the recorded OpenStreetMap statement.
|
|
192
|
+
*
|
|
193
|
+
* The source keeps its own group as well, because that text also credits the
|
|
194
|
+
* publishers of the address skeletons.
|
|
195
|
+
*/
|
|
196
|
+
const CONTENT_LICENSES: Readonly<Record<string, readonly string[]>> = {
|
|
197
|
+
"spliced-sub-venue": [OSM_LICENSE],
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The licenses a source owes attribution under: the one its rows state and those its contents carry.
|
|
202
|
+
*/
|
|
203
|
+
function attributedLicenses(record: EffectiveSourceRecord): string[] {
|
|
204
|
+
const current = currentSourceName(record.source) ?? record.source
|
|
205
|
+
|
|
206
|
+
return [record.license, ...(CONTENT_LICENSES[current] ?? [])].filter((license) => {
|
|
207
|
+
const summary = summarizeLicense(license)
|
|
208
|
+
|
|
209
|
+
return !summary.recognized || summary.obligations.includes(LicenseObligation.Attribution)
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
181
213
|
/**
|
|
182
214
|
* The attributions owed by the sources that reached the trainer, grouped by license.
|
|
183
215
|
*
|
|
184
216
|
* Only `trainingSources` are read, so a source the corpus holds and the run never drew owes no attribution.
|
|
185
217
|
* A license is included when `summarizeLicense` records an attribution obligation for it, or
|
|
186
218
|
* when it is unrecognized, since an unrecognized license's obligations are unknown rather than absent.
|
|
219
|
+
*
|
|
220
|
+
* A source joins the group of every license in {@linkcode CONTENT_LICENSES} beside its own.
|
|
187
221
|
*/
|
|
188
222
|
export function requiredAttributions(sources: readonly EffectiveSourceRecord[]): RequiredAttribution[] {
|
|
189
223
|
const byLicense = new Map<string, string[]>()
|
|
@@ -191,11 +225,9 @@ export function requiredAttributions(sources: readonly EffectiveSourceRecord[]):
|
|
|
191
225
|
for (const record of sources) {
|
|
192
226
|
if (record.emittedRows <= 0) continue
|
|
193
227
|
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
byLicense.set(record.license, [...(byLicense.get(record.license) ?? []), record.source])
|
|
228
|
+
for (const license of attributedLicenses(record)) {
|
|
229
|
+
byLicense.set(license, [...(byLicense.get(license) ?? []), record.source])
|
|
230
|
+
}
|
|
199
231
|
}
|
|
200
232
|
|
|
201
233
|
return [...byLicense]
|
|
@@ -124,8 +124,21 @@ export const REGISTER_SECTORS = [
|
|
|
124
124
|
"environment-industry",
|
|
125
125
|
"finance",
|
|
126
126
|
"food-pharma",
|
|
127
|
+
/**
|
|
128
|
+
* Text a language model generated, which a corpus source reads as rows.
|
|
129
|
+
*/
|
|
130
|
+
"generated-text",
|
|
131
|
+
/**
|
|
132
|
+
* A gazetteer, map or address collection that compiles many publishers' data under its own
|
|
133
|
+
* terms, such as OpenStreetMap, Overture Maps, Who's On First, GeoNames or OpenAddresses.
|
|
134
|
+
*/
|
|
135
|
+
"geodata-aggregate",
|
|
127
136
|
"health",
|
|
128
137
|
"mixed-regulatory",
|
|
138
|
+
/**
|
|
139
|
+
* A postal operator's or postal union's publication, such as a postcode file or an addressing standard.
|
|
140
|
+
*/
|
|
141
|
+
"postal",
|
|
129
142
|
"procurement-grants",
|
|
130
143
|
"professional-licensing",
|
|
131
144
|
"property-building",
|
|
@@ -184,6 +197,13 @@ export const ResearchPass = {
|
|
|
184
197
|
* It located each authority and did not read its terms.
|
|
185
198
|
*/
|
|
186
199
|
DecoderExposureSurvey: "2026-10-03-decoder-exposure",
|
|
200
|
+
/**
|
|
201
|
+
* The mapping of each corpus source to the publications its rows are built from.
|
|
202
|
+
*
|
|
203
|
+
* It added a row for every publication a corpus source reads that no earlier pass recorded,
|
|
204
|
+
* and it read no terms, so each such row's decision stays `unchecked`.
|
|
205
|
+
*/
|
|
206
|
+
CorpusInputMapping: "2026-10-17-corpus-input-mapping",
|
|
187
207
|
} as const
|
|
188
208
|
|
|
189
209
|
/**
|
|
@@ -151,7 +151,7 @@ function makeRecipe(name: string, country: "PK" | "BD", locale: string, descript
|
|
|
151
151
|
|
|
152
152
|
if (opts.count !== undefined && emitted >= opts.count) break
|
|
153
153
|
|
|
154
|
-
const components = componentsForOSMRow(tuple as Parameters<typeof componentsForOSMRow>[0])
|
|
154
|
+
const components = componentsForOSMRow(tuple as Parameters<typeof componentsForOSMRow>[0], country)
|
|
155
155
|
const rendering = components ? renderSouthAsiaRegister(components, random) : null
|
|
156
156
|
|
|
157
157
|
if (!rendering) {
|