feat(11-02): implement NUTS-1 to Bundesland derivation

bundeslandFromRegion() derives one of the 16 Bundesland names from a
region's NUTS-1 (3-char) prefix; nutsPrefixFor() reverses a Bundesland
name back to its prefix for the query builder. Null-safe throughout —
7/7 tests green, validated against real region samples from the live DB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-21 15:59:35 +02:00
parent 3dec35f498
commit d12e1c9aa9
@@ -0,0 +1,67 @@
/**
* NUTS-1 -> Bundesland derivation (FILTER-02, D-02, Pitfall 1).
*
* `Tender.bundesland` is 100% NULL in the live DB (0/1671 rows, verified)
* because the Phase-10 normalizer explicitly deferred NUTS->Bundesland
* mapping to this phase (`tender-normalizer.service.ts` previously set
* `bundesland = null` unconditionally). Without this derivation the
* Bundesland filter would return a constant zero result and look like a
* bug rather than an empty column.
*
* `region` holds a EU NUTS code (e.g. `DE212`); the first 3 characters
* (`DE` + one Bundesland-level digit/letter) form the stable NUTS-1
* region — the standardized, deterministic EU classification scheme,
* unlike a PLZ-range heuristic (PLZ zones cross Bundesland borders).
*
* Validated against real region samples pulled from the live DB
* (`docker exec tessera-ctl-db-1 psql`, Assumption A1):
* DE212 (60 rows) -> Bayern (Oberbayern)
* DE300 (56 rows) -> Berlin
* DE600 (38 rows) -> Hamburg
* DE712 (34 rows) -> Hessen (Darmstadt)
* DEA22 (24 rows) -> Nordrhein-Westfalen (Köln)
*/
export const NUTS1_BUNDESLAND: Record<string, string> = {
DE1: 'Baden-Württemberg',
DE2: 'Bayern',
DE3: 'Berlin',
DE4: 'Brandenburg',
DE5: 'Bremen',
DE6: 'Hamburg',
DE7: 'Hessen',
DE8: 'Mecklenburg-Vorpommern',
DE9: 'Niedersachsen',
DEA: 'Nordrhein-Westfalen',
DEB: 'Rheinland-Pfalz',
DEC: 'Saarland',
DED: 'Sachsen',
DEE: 'Sachsen-Anhalt',
DEF: 'Schleswig-Holstein',
DEG: 'Thüringen',
};
/**
* Derive the Bundesland name from a NUTS region code's NUTS-1 (3-char)
* prefix. Null-safe: null/undefined/empty region (17%+ of live rows) and
* unrecognized prefixes return `null` instead of throwing — never crash
* on the common missing-region case.
*/
export function bundeslandFromRegion(region?: string | null): string | null {
if (!region) return null;
const prefix = region.slice(0, 3).toUpperCase();
return NUTS1_BUNDESLAND[prefix] ?? null;
}
/**
* Reverse lookup: Bundesland display name -> NUTS-1 prefix, used by the
* query builder for `region: { startsWith: nutsPrefixFor(bundesland) }`
* as a fallback path (and available even independent of the backfilled
* `bundesland` column).
*/
export function nutsPrefixFor(bundesland: string): string | null {
for (const [prefix, name] of Object.entries(NUTS1_BUNDESLAND)) {
if (name === bundesland) return prefix;
}
return null;
}