/** * @copyright Sister Software * @license AGPL-3.0 * @author Teffen Ellis, et al. * * `house-venue` recipe — synthetic house_number+venue+street co-occurrence rows: tuples → * {@link synthesizeHouseVenueRow} → aligned LabeledRow. The v0.6.3 corrective companion to the * no-street recipe: every row includes both house_number and venue, restoring the house_number * signal that no-street's distributional shift cost the model. Ported from the root build script * it replaced. */ import { makeLcg } from "@mailwoman/core/utils" import { alignAndWrite, readTuples, recipeSourceID, SYNTHETIC_TUPLE_LICENSE as LICENSE, type CorpusRecipe, } from "#recipes/scaffold" import { defaultRecipeSource } from "#recipes/sources" import { synthesizeHouseVenueRow, type HouseVenueBaseTuple } from "#synthesizers/house-venue" import { SurfaceOrigin } from "#types" /** * The venue name and house number are drawn and joined to a tuple's admin components. * * No register asserts that this venue stands at this number, so the row teaches the * co-occurrence shape rather than reporting a published record. */ const HOUSE_VENUE_PROVENANCE = { register: null, surface: SurfaceOrigin.Invented, // An invented row has no underlying record to name. baseSourceID: null, } /** * Recipe registered with the corpus builder. * * See the file header for the parse behavior it exercises. * See `description` below for the generated surface form. */ export const houseVenueRecipe: CorpusRecipe = { name: "house-venue", description: "House_number+venue co-occurrence rows: tuples → synthesizeHouseVenueRow → aligned LabeledRow", mode: "tuples", async run(opts, write) { if (!opts.input) throw new Error("house-venue recipe requires --input ") // The root build script this recipe replaced seeded the LCG via makeRandom(opts.seed) (s = seed). const random = makeLcg(opts.seed) const source = opts.sourceName ?? defaultRecipeSource("synth-house-venue") let read = 0 let emitted = 0 let skipped = 0 for await (const tuple of readTuples(opts.input)) { read++ // FR renders without a region (postcode-before-locality tail — the run-2 contingency), // so an empty region is valid there and stays required everywhere else. if (!tuple.locality || !tuple.postcode || !tuple.country || (!tuple.region && tuple.country !== "FR")) { skipped++ continue } for (let v = 0; v < opts.variants; v++) { const synth = synthesizeHouseVenueRow(tuple as HouseVenueBaseTuple, { random }) if (!synth) { skipped++ continue } const ok = alignAndWrite( write, { raw: synth.raw, components: synth.components, country: tuple.country, locale: synth.locale, source, source_id: recipeSourceID(source, { locality: tuple.locality, region: tuple.region, postcode: tuple.postcode, country: tuple.country, template: synth.template, v: String(v), }), corpus_version: "0.4.0", license: LICENSE, }, synth.template, HOUSE_VENUE_PROVENANCE ) if (ok) { emitted++ } else { skipped++ } } } return { read, emitted, skipped } }, }