/** * @copyright Sister Software * @license AGPL-3.0 * @author Teffen Ellis, et al. * * German address synthesizer — multi-locale coverage (night-shift 2026-06-02, DE-1). * * The neural model is out-of-distribution on German: it truncates `Straußstraße`→`Strau` (exits at * the ß-piece boundary), absorbs the house number into the street (`Hauptstraße 5` → one span), * and mis-tags the native-order house number as a postcode (`Prenzlauer Allee 36, 10405 Berlin` → * postcode `36`). The cause is ORDER: the model was trained US+FR (house-number-FIRST, * postcode-AFTER-city), and never saw the German convention (house-number-AFTER-street, * postcode-BEFORE-city). DE-0 confirmed the tokenizer round-trips German orthography cleanly, so * this is a coverage gap, not a tokenizer ceiling. * * This generator produces the missing signal as a small targeted supplement shard * (synthesis-as-supplement discipline: weight < 0.25, one-and-done). It does NOT synthesize * German street names (German morphology is hard to fake) — it takes REAL German component tuples * (from OpenAddresses Berlin/Saxony) and renders them in idiomatic German order via the OpenCage * `DE` template (`formatAddress(..., "DE")` → `"Straußstraße 27, 12623 Berlin"`). The corpus * aligner turns the row into BIO labels; every emitted component surface form occurs verbatim in * `raw` so alignment lands. */ import type { CanonicalRow } from "@mailwoman/corpus/types"; /** * A real address tuple (e.g. one OpenAddresses row): street + locality required, rest optional. */ export interface LocaleBaseTuple { house_number?: string; street: string; locality: string; /** * A sub-locality that sits BELOW the locality (a suburb / district). NZ is the case that needs it: the OA DISTRICT * column holds the city (`Auckland`) and CITY holds the suburb (`Birkenhead`), so the real envelope carries both (`31 * Rawene Road, Birkenhead, Auckland`). Rendered between street and locality in both orders when present. */ dependent_locality?: string; region?: string; postcode?: string; } /** * @deprecated Alias — use LocaleBaseTuple. */ export type GermanBaseTuple = LocaleBaseTuple; export interface SynthesizedLocaleRow { raw: string; components: CanonicalRow["components"]; locale: string; } /** * @deprecated Alias — use SynthesizedLocaleRow. */ export type SynthesizedGermanRow = SynthesizedLocaleRow; export interface LocaleSynthesisOpts { random?: () => number; /** * Rendering order for the SAME components. `"native"` (default) uses the country's own template (DE → * house-AFTER-street, postcode-BEFORE-city). `"international"` renders house-FIRST, postcode-AFTER-city — the US/GB * layout that international feeds, US-centric systems, and our own OpenAddresses de-sample impose on non-US * addresses. Training both teaches the model that a German address can arrive either way, so the eval's US-order * rendering stops reading as a collapse. See `docs/articles/evals/resolver-geo/2026-06-06-anchor-pilot.md` (the * order-artifact correction). */ order?: "native" | "international"; /** * Postcode surface shape. `"conventional"` (default) canonicalizes to the country's rendered form (NL: OA's glued * `1011AB` → the spaced `1011 AB`); `"as-source"` keeps the source's own surface — the form OA (and the OA-derived * evals) feed, which for NL is 100% glued. Only NL differs today; every other country passes through identically * either way. Mixing both teaches the two-letter-suffix `1012 LM` shape AND the glued feed shape (#241 — the model * currently glues the suffix onto the city). */ postcodeShape?: "conventional" | "as-source"; /** * How the NATIVE-order render joins street and house number. The OpenCage ES template comma-joins (`Calle Mayor, 12` * — the official Spanish convention); OA-derived feeds and our ES eval space-join (`CALLE MAYOR 12`, the observed * form on all 3,000 eval rows). `"template"` (default) keeps the template's own join; `"space"` collapses `, * ` → ` ` after rendering. Countries whose template already space-joins * (DE/IT/NL) render identically under both. Mixing both stops an ES shard from teaching the comma as THE street→house * boundary signal (#241 format-diversity audit). International order ignores this (the US template is already * house-first space-joined). */ nativeHouseJoin?: "template" | "space"; } /** * @deprecated Alias — use LocaleSynthesisOpts. */ export type GermanSynthesisOpts = LocaleSynthesisOpts; /** * Render one real tuple into an idiomatic, locale-ordered `{raw, components}` row via the OpenCage `country` template * (DE → house-after-street + postcode-before-city; ES/IT the same; GB house-first; NL carries the `1012 LM` postcode), * with light variation (drop house number / postcode some of the time). Returns `null` when the tuple is too thin or a * component wouldn't align cleanly. * * Region handling is order-dependent: NATIVE order omits it (the native template absorbs the admin region into the * postcode/city line, so it rarely renders verbatim and would break BIO alignment), while INTERNATIONAL order includes * it in the tail ("City, Region Postcode" — the US/feed layout the eval uses; v0.9.3 / #327). * * Pass `opts.order: "international"` to render the same components house-first / postcode-after-city instead (see * {@link LocaleSynthesisOpts.order}) — the layout international feeds impose on foreign addresses, and the one a * native-order-trained model treats as a "collapse." */ export declare function synthesizeLocaleRow(base: LocaleBaseTuple, country: string, opts?: LocaleSynthesisOpts): SynthesizedLocaleRow | null; /** * German wrapper over {@link synthesizeLocaleRow}. Kept for the build-german-shard caller + tests. */ export declare function synthesizeGermanRow(base: LocaleBaseTuple, opts?: LocaleSynthesisOpts): SynthesizedLocaleRow | null; //# sourceMappingURL=german.d.ts.map