Parsing German Street and House Number Formats in Python

TL;DR: German addresses put the number after the street, glue the street type onto the name, and use hyphenated ranges and letter suffixes freely. Anchor the number at the end of the line, split the compound suffix separately, and keep ß handling in the match key rather than in the parser. The regional context is parsing European address conventions.

The Pattern

from __future__ import annotations

import re

# Street types that appear glued to the end of the name, longest first so
# "strasse" is preferred over "str" when both would match.
STREET_TYPES = (
    "straße", "strasse", "str", "gasse", "weg", "allee", "platz", "ring",
    "damm", "ufer", "chaussee", "steig", "pfad", "markt",
)
_TYPES_ALT = "|".join(sorted(STREET_TYPES, key=len, reverse=True))

DE_LINE_RE = re.compile(
    r"""
    ^(?P<street>.+?)                       # lazy: stops at the number
    \s+
    (?P<number>
        \d+                                # 12
        (?:\s*[-/]\s*\d+)?                 # 12-14 or 12/14 (a range)
        \s*[a-zA-Z]?                       # 12a
    )
    \s*$                                   # the number closes the line
    """,
    re.VERBOSE,
)

SUFFIX_RE = re.compile(rf"(?P<base>.+?)(?P<suffix>{_TYPES_ALT})$", re.IGNORECASE)

The lazy .+? on the street combined with the end-anchored number is what makes this robust. A greedy street group would consume the number, and an unanchored number group would match the first digits it found — which in Straße des 17. Juni 12 is the wrong ones entirely.

Compound Suffixes Are the Real Difference

Separate suffix versus glued suffix Two address lines broken into components. The English line splits into number, name and a separate suffix token that a token-level list can find. The German line has the suffix glued onto the name, so a token-level suffix list finds nothing and a second string-level split is required. English — the suffix is its own token 12 Station Road a token list finds it directly German — the suffix is glued on Bahnhof straße 12 needs a string split, not a token list A parser that only looks for suffix tokens leaves the suffix field empty on almost every German record.

The symptom of getting this wrong is subtle: the street name is captured correctly, the address renders correctly, and the suffix column is empty. Nothing fails until a join or a deduplication key that expects a suffix quietly stops matching.

The Parser

from dataclasses import dataclass


@dataclass(frozen=True)
class GermanAddress:
    street_base: str      # "Bahnhof"
    street_suffix: str    # "straße"
    street_full: str      # "Bahnhofstraße" — what goes on the label
    house_number: str     # "12a" or "12-14", always text


def parse_de(line: str) -> GermanAddress | None:
    """Parse a German-convention delivery line into its components."""
    text = " ".join(line.replace(",", " ").split())
    m = DE_LINE_RE.match(text)
    if m is None:
        return None

    street = m.group("street").strip()
    number = re.sub(r"\s+", "", m.group("number"))

    sm = SUFFIX_RE.match(street)
    if sm is None:
        return GermanAddress(street, "", street, number)
    return GermanAddress(
        street_base=sm.group("base").strip(),
        street_suffix=sm.group("suffix"),
        street_full=street,
        house_number=number,
    )

Keeping street_full alongside the split components avoids a common regression: rebuilding the name by concatenation loses the original capitalisation and any internal hyphen, and a label printed from the reconstruction differs subtly from what the customer typed.

Where the Pattern Can Go Wrong

Three lines, three outcomes Three inputs. Bahnhofstraße 12a splits into street and number. Straße des 17. Juni 12 contains a number inside the name and still splits correctly because the number group is anchored at the end. 12 Bahnhofstraße, entered in Anglophone order, produces no match and is routed to a swap-and-retry step. Bahnhofstraße 12a street=Bahnhofstraße · number=12a Straße des 17. Juni 12 number=12 — the end anchor saves it 12 Bahnhofstraße no match — swap and retry, and record it

Numbers That Are Not Integers

Input Meaning Store as
12 a single building "12"
12a a subdivided plot "12a"
12-14 one entity spanning three plots "12-14"
12/1 building 12, staircase 1 — Austrian "12/1"
12 a the same as 12a, spaced normalise to "12a"

Every row in that table breaks if the column is typed as an integer, and the failure is silent: the row is either dropped at load or truncated to 12, and both look like clean data afterwards. Text storage plus a normalisation of internal whitespace covers all five.

Austria and Switzerland Differ Slightly

The German convention extends across Austria and German-speaking Switzerland with two practical differences worth handling explicitly.

Austrian addresses commonly carry a staircase and door after the house number — 12/3/14 meaning building 12, staircase 3, door 14 — and those trailing components are delivery information rather than decoration. Capture them as unit components rather than letting them fall into the house-number group, where they will confuse any downstream range logic.

Swiss addresses use the same street-then-number order but do not use ß at all; it is always written ss. That means a Swiss corpus needs no eszett folding, and, more usefully, that a record containing ß is unlikely to be Swiss — a small signal, but a free one when country inference is uncertain.

Both countries also use four-digit postcodes rather than Germany’s five, which is the strongest available discriminator among the three when the country field is missing.

Testing Against Real Street Names

Four kinds of street name a test set needs Four categories. Simple compounds such as Bahnhofstraße are the common case. Names containing numbers such as Straße des 17. Juni test the end anchor. Hyphenated personal names such as Rosa-Luxemburg-Platz test the suffix split. Non-compound names such as Unter den Linden have no suffix at all. simple compound Bahnhofstraße · Kirchgasse the 80% case contains a number Straße des 17. Juni tests the end anchor hyphenated name Rosa-Luxemburg-Platz tests the suffix split no suffix at all Unter den Linden · Am Markt suffix field legitimately empty

Edge Cases and Failure Modes

Street names ending in a number. Straße des 17. Juni has a number inside the name. The end anchor handles it correctly; a pattern searching for the first digit does not.

A house number written before the street. Data entered by an Anglophone user sometimes arrives as 12 Bahnhofstraße. Detect it by the leading digits and swap, but record that the swap happened — silent reordering makes a later mismatch impossible to explain.

Abbreviated suffixes. Bahnhofstr. is common and the trailing period must be tolerated. The suffix list includes str for exactly this reason, and the period is stripped before matching.

Eszett in the match key. Straße and Strasse must produce the same key, which is a folding concern rather than a parsing one — see removing diacritics from addresses in Python. The parser should leave the character exactly as it found it.

Why the Suffix Split Is Worth the Effort

A parser that captures the street name and leaves the suffix empty produces addresses that render perfectly and match badly, which is the most expensive combination.

The reason is that abbreviation is common in German data and unpredictable in direction. The same street appears as Bahnhofstraße, Bahnhofstr., Bahnhofstrasse and occasionally Bahnhofstr, and only a parser that knows where the name ends and the type begins can normalise all four onto one key. Without the split, they are four distinct strings and four distinct records.

The split also enables the checks that catch a regression. On a German sample, the share of records with a recognised street type should be high and stable — well above ninety percent — and a drop is an immediate signal that the suffix list or the splitting logic has broken. That assertion is far more sensitive than a parse-rate check, because a broken splitter does not reduce the parse rate at all.

Finally, it makes the suffix list maintainable. Regional street types — Steig, Pfad, Chaussee — can be added as they appear in a market, and the effect of each addition is measurable in that same coverage figure rather than being a matter of judgement.

Integration Note

This parser is selected by country, which means the country must be determined before it runs — from an explicit field where one exists, and from the postcode shape otherwise, as described in parsing European address conventions. Its output feeds the same canonical-key derivation as every other market, and the street_base and street_suffix split is what lets that key normalise Bahnhofstr. and Bahnhofstraße onto one value without a special case in the key builder itself.

The same coverage figure is the fastest way to evaluate a candidate addition: add the type, re-run the sample, and keep the change only if the recognised-type share moves. That turns the suffix list from a matter of opinion into something with a test behind it.

Rendering Back Out

Parsing is only half the round trip, and the rendering side has one rule that is easy to get wrong: the street name and number go back together in the original order, with the number last, and the street type stays glued to the name.

That sounds obvious and is routinely broken by code that was written for a US schema and reused. A renderer that emits number-then-street produces 12 Bahnhofstraße, which is comprehensible to a human, unusual on a German label, and a mismatch against every stored form of the same address.

Keeping the reconstructed line beside the components — the street_full field in the parser above — removes the temptation to rebuild it at all. The label uses the stored line, the matching uses the components, and neither has to trust the other’s formatting.