/** * @copyright Sister Software * @license AGPL-3.0 * @author Teffen Ellis, et al. * * Shared scaffolding for corpus recipes: the seeded LCG PRNG, the tuple reader and * the canonical → `alignRow` → `LabeledRow` jsonl emit step. A recipe ({@link CorpusRecipe}) * supplies only its row construction and filter. The `mailwoman corpus slice ` command * supplies the I/O. */ import type { PathBuilderLike } from "path-ts"; import type { AsyncChunkIterator, AsyncDataResource } from "spliterator"; import { AsyncSequence } from "spliterator"; import type { SurfaceOrigin } from "#types"; /** * {@link stableSourceIDFromParts} under the name the recipes use, for arbitrary * disambiguator keys (e.g. a variant index `v`) that are not `ComponentTag`s. */ export declare function recipeSourceID(adapterID: string, parts: Record): string; /** * Where a country's convention writes the postcode inside the `«locality», «region»[, «country»]` admin tail. * * The position changes which tag the same digits receive, so a recipe emitting * one placement teaches one family of countries. * Each value is attested by a gauntlet board row: * * - `leading` — `«postcode» «locality», «region»`. * This is the default placement. * It also describes every tuple written before this field existed. * - `after_locality` — `«locality» «postcode», «region»`. * - `after_region` — `«locality», «region» «postcode»`. */ export type PostcodePlacement = "leading" | "after_locality" | "after_region"; /** * A (locality, region, postcode, country) source tuple — the input to tuples-mode recipes. */ export interface RecipeTuple { locality?: string; /** * The source's segment before the locality. * * Its inclusion prevents every recipe row from beginning with the locality * and teaching that the first segment is the locality. */ dependentLocality?: string; region?: string; postcode?: string; country?: string; /** * Defaults to `leading` when absent. * * That matches every tuples file written before this field existed. */ postcodePlacement?: PostcodePlacement; /** * `source_id` of the record this tuple was extracted from, when the extraction included it. * * A recipe forwards it as `baseSourceID`, which is the only way a `composed` * row can name the record behind it. * Every tuples file written before this field existed omits it, so a recipe reading one passes `null` * and its rows are unlinkable to their source records by anything but the `raw` string (#2359). */ sourceID?: string; [k: string]: unknown; } /** * One CSV record, keyed by its lower-cased header name, every value trimmed. * * An undeclared column reads as `undefined`, a declared but unreached column as `""`. */ export type CSVRecord = Record; /** * Line breaks inside a value become single spaces. * The function trims every value. * * The pattern matches exactly `\r` and `\n`: widening to `\s+` would silently rewrite values on * rows with no line break at all (OA's IA extract writes `north`, three spaces, `main street`), * and `scaffold.test.ts` pins both directions. */ export declare function withoutLineBreaks(record: CSVRecord): CSVRecord; /** * Read a CSV as header-keyed records. * * Returns the spliterator's own {@linkcode AsyncSequence} so a caller composes * `take`/`drop`/`filter` onto it. * An `async function*` wrapper would cost an async frame per row and remove those ops. */ export declare function readCSVRecords(source: AsyncDataResource | AsyncChunkIterator): AsyncSequence; /** * {@link readCSVRecords} over one member of a zip archive. * * A source a checkout has not cached yields no rows after warning, while a recipe * that ends up with no tuples at all still throws. */ export declare function readZippedCSVRecords(archivePath: PathBuilderLike, entryName: string): AsyncSequence; /** * License stamped on the synthetic tuple-derived recipe outputs. * * The output is generated but inherits the terms of the real tuples it derives from, * so the attribution travels with it. */ export declare const SYNTHETIC_TUPLE_LICENSE = "Synthetic \u2014 derived from CC-BY / public-domain input tuples"; /** * The surface key shared by the Norwegian recipes (`no-fragment`, `no-street-led`). * * Must match the Norwegian digit board's `norm_surface` (NFC, lowercase, collapse whitespace, * keep diacritics); folding them away would collapse `Tømmerlien` to `tommerlien`, * never match the board's reserved `tømmerlien`, and leak the train/eval split silently. */ export declare const foldNOSurface: (value: string) => string; /** * A cached OpenAddresses extract: the zip and the CSV member. */ export interface OATupleSource { zip: PathBuilderLike; csv: string; } /** * The four base fields every OA tuple reader extracts; `postcode` is `""` when the row * contains no postcode and {@link ReadOATuplesOptions.requirePostcode} is unset. */ export interface OATupleFields { house_number: string; street: string; locality: string; postcode: string; } export interface ReadOATuplesOptions { /** * Stop after this many distinct tuples. * * The `break` closes the reader and releases the archive. * GB-scale countrywide extracts need this behavior. */ limit?: number; /** * Drop rows without a postcode, for the reversed-order and balance readers * where the postcode drives the rendering. */ requirePostcode?: boolean; /** * Fold the postcode into the dedup key. * * The default key is `${house_number}|${street}|${locality}`, lower-cased. */ dedupIncludesPostcode?: boolean; /** * Shape the recipe's tuple from the base fields, raw record and lower-cased dedup key. */ extra: (fields: OATupleFields, row: CSVRecord, key: string) => T; } /** * Stream distinct tuples out of a cached OA zip, the reader the OA-skeleton recipes * (`street-affix`, `unit`, `country-balanced`, `fr-order`) share. * * Field reads, filters, dedup keys and row order keep each recipe's output byte-identical. */ export declare function readOATuples(source: OATupleSource, options: ReadOATuplesOptions): Promise; /** * Stream-parse a tuples jsonl file, yielding each parsed object (blank/invalid lines skipped). */ export declare function readTuples(input: PathBuilderLike): AsyncSequence; /** * A canonical row as the recipes assemble it, before `alignRow` turns it into a `LabeledRow`. */ export interface CanonicalRecipeRow { raw: string; components: Record; country: string; locale?: string; source: string; source_id: string; corpus_version?: string; license?: string; } /** * Emit one line of a recipe's output. * * The delimiter is supplied separately, so pass only the content, without appending `"\n"`. */ export type WriteRecipeLine = (line: string) => void; /** * A sink the recipe writer sends each output line to. * A `WriteStream` or a test's array push can satisfy it. */ export interface RecipeLineSink { write(chunk: string): unknown; } /** * Bind {@linkcode WriteRecipeLine} to a sink, supplying the delimiter. * * A concatenation would stringify a non-string chunk through `toString()` and corrupt its bytes. */ export declare function createRecipeLineWriter(sink: RecipeLineSink): WriteRecipeLine; /** * What a recipe records about the rows it writes. * * `register` and `surface` answer separate questions. * A collapsed result would report a real record as fabricated. */ export interface RecipeProvenance { /** * The published register the underlying record came from, or `null` when the row names none. */ register: string | null; /** * How the written `raw` string was produced. */ surface: SurfaceOrigin; /** * `source_id` of the row this was derived from. * * This field is required. * `null` is a statement rather than an omission. * * A `composed` row reorders fields from a real record, so it has an underlying record by definition. * `null` means the recipe did not preserve that record's id. * * The cost of `null` is measurable. * Over `v0.6.0-register-surface` it is `null` for 4,176,539 of the 4,206,561 `composed` rows, * 99.3%, and every leak measurement over a composed source is therefore a lower bound: two rows * composed from one underlying address under different `source_id`s look like unrelated rows. * * `holdout-overlap.run.ts` then compares their `raw` strings. * That misses any pair the recipe rendered differently (#2359). */ baseSourceID: string | null; } /** * The register a tuple-reading recipe was invoked with. * * It throws when the flag is absent, naming the recipe, because a default would * stamp one register's id on another register's records. */ export declare function requireRegister(opts: RecipeOptions, recipe: string): string; /** * Run a canonical row through `alignRow` and, on success, write the `LabeledRow` * with its recipe id and provenance as one jsonl line. * * The provenance columns are flat because the parquet schema is flat. */ export declare function alignAndWrite(write: WriteRecipeLine, canonical: CanonicalRecipeRow, recipe: string, provenance: RecipeProvenance): boolean; /** * Parsed options a recipe's `run` receives. * * Common fields + the union of recipe-specific flags. */ export interface RecipeOptions { output: string; seed: number; variants: number; input?: PathBuilderLike; count?: number; golden?: boolean; sourceName?: string; /** * The published register the `--input` tuples were extracted from. * * {@link requireRegister} refuses a run that omits it rather than recording a guess. * A recipe that generates its rows from this repository's own tables declares its register in code. */ register?: string; houseNumberProb?: number; pmbRatio?: number; militaryRatio?: number; reversedFraction?: number; edgesDir?: string; country?: string; intlFraction?: number; /** * `german`: fraction of native-order rows rendered with no commas at all. * The default is 0.3. */ commaFreeFraction?: number; /** * `german`: fraction of rows whose `dependent_locality` is a WOF Ortsteil of the tuple's locality. * * The default is 0.3. * It is 0 when no admin database is readable. */ ortsteilFraction?: number; /** * `german`: the WOF admin database the Ortsteil pool is read from. * * Default `$MAILWOMAN_DATA_ROOT/db/wof/admin-global-priority-importance.db`. */ adminDB?: string; /** * `locale`: fraction of rows that append an explicit country surface form + a `country` component. * * Default 0. */ countryFraction?: number; /** * `locale`: tri-state override of the per-part `districtAsLocality` mapping. * * `undefined` leaves each `COUNTRY_SOURCES` part's own value untouched. * `true` or `false` forces that value on every part read this run. */ districtAsLocality?: boolean; bareProb?: number; hnProb?: number; communes?: string; /** * `fr-lieudit`: BAN `adresses-.csv` directory. * * Default `$MAILWOMAN_DATA_ROOT/corpus/sources/ban`. */ banDir?: PathBuilderLike; multilocaleCount?: number; /** * `fr-fragment` / `no-fragment` / `no-street-led`: the eval board's reserved street-surface list. * * It is required for those recipes, because a recipe output that trains on * its own eval set measures memorization. */ excludeSurfaces?: PathBuilderLike; /** * `no-fragment`: share of rows that are counter-distribution (bare locality or bare postcode). */ counterProb?: number; /** * `no-fragment` knob 3: emit N copies of each street+number row whose number has >= * longNumberMinDigits digits (oversample the failing long-number class). * * Default 1 = no boost. */ longNumberBoost?: number; /** * `no-fragment` knob 3: minimum digit count for a number to count as "long" and be boosted. * * Default 3. */ longNumberMinDigits?: number; /** * `sub-venue`: the sub-venue lexicon JSON. * * Default = the committed `corpus/data/sub-venue-lexicon.json`, resolved through the package manifest so it works * from the source tree and from `out/`. */ lexicon?: string; /** * `sub-venue`: directory of `sub-venue-extract` JSONLs, one per region. * * Default `$MAILWOMAN_DATA_ROOT/sub-venue/extracts`. */ extractsDir?: string; /** * `sub-venue`: the `poi.db` spatial layer, read for the en-US and fr-FR venue + confound * pools (the two of poi.db's four countries this recipe has legs for). * * Default `$MAILWOMAN_DATA_ROOT/db/poi/poi.db`. */ poiDB?: string; /** * `sub-venue`: GB/US/FR address-context tuples jsonl. * * Default the house-venue v3 tuples (`$MAILWOMAN_DATA_ROOT/corpus/intermediate/house-venue-tuples-v3.jsonl`); * DE and ES read OpenAddresses directly. */ subVenueTuples?: string; /** * `sub-venue`: share of emitted rows that are confound negatives. * * Default 0.3. */ negativeFraction?: number; } /** * Tally a recipe returns. */ export interface RecipeStats { read?: number; emitted: number; skipped: number; /** * Rows dropped because their street surface is reserved by an eval board. * * It is separate from `skipped` so a nonzero value is the audit trail that the train/eval split fired. */ contaminated?: number; } /** * A single declared recipe-specific option flag (for the command's --help). */ export interface RecipeOption { flag: string; description: string; } /** * A corpus recipe: its identity, input mode and synthesis `run`. */ export interface CorpusRecipe { /** * Recipe id, e.g. "street", "po-box" — the `` positional. */ name: string; /** * One-line description for `--list` / help. */ description: string; /** * `tuples` reads `--input` jsonl; `generate` self-generates `--count` rows. */ mode: "tuples" | "generate"; /** * Recipe-specific flags this recipe honors (documentation only). */ options?: RecipeOption[]; /** * Build by creating the recipe's PRNG from `opts.seed`. * Synthesize rows and emit each one through `write`. */ run(opts: RecipeOptions, write: WriteRecipeLine): Promise; } //# sourceMappingURL=scaffold.d.ts.map