Skip to content

Addresses

rigour.addresses

This module provides a set of tools for handling postal/geographic addresses. It includes functions for comparing addresses, normalising them, and for formatting addresses given in parts for display as a single string.

Address comparison

Score whether two address strings (or two sets of them) denote the same place:

from rigour.addresses import compare_address, match_addresses

score = compare_address("Bahnhofstr. 10, Augsburg", "Bahnhofstrasse 10, 86150 Augsburg")
match = match_addresses(
    ["Bahnhofstrasse 10, Augsburg"],
    ["Bahnhofstrase 10, 86150 Augsburg", "P.O. Box 71, Augsburg"],
)
# match.result == "Bahnhofstrase 10, 86150 Augsburg"
# match.detail == "bahnhofstrasse~bahnhofstrase 10 augsburg +86150"

compare_address_many is the same pairing returning only the score.

The comparison runs in native code over analyzed tokens — see compare_address for the mechanics and score semantics.

The same analysis backs a keying surface: use address_fingerprint to reduce equivalent renderings of an address to one deterministic string for deduplication or graph node identity:

from rigour.addresses import address_fingerprint

key = address_fingerprint("Main Boulevard 5, Syrian Arab Republic")
# "main blvd 5 sy" — same key as for "Main Blvd. 5, Syria"

Postal address formatting

This set of helpers is designed to help with the processing of real-world addresses, including composing an address from individual parts, and cleaning it up.

from rigour.addresses import format_address_line

address = {
    "road": "Bahnhofstr.",
    "house_number": "10",
    "postcode": "86150",
    "city": "Augsburg",
    "state": "Bayern",
    "country": "Germany",
}
address_text = format_address_line(address, country="DE")
Acknowledgements

The address formatting database contained in rigour/data/addresses/formats.yml is derived from worldwide.yml in the OpenCageData address-formatting repository. It is used to format addresses according to customs in the country that is been encoded.

AddressMatch

The best pair out of a list×list address comparison, with the evidence that produced it.

__doc__ = 'The best pair out of a list×list address comparison, with the\nevidence that produced it.' class-attribute

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.

__module__ = 'rigour._core' class-attribute

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.

detail property

One-line alignment summary over comparable token forms: aligned tokens in query order (berlin when identical, boulevard~blvd when aligned by edit distance or class equivalence), then -tok for query-only and +tok for result-only tokens.

query property

Query-side address of the winning pair, as supplied.

result property

Result-side address of the winning pair, as supplied.

score property

Similarity of the winning pair in [0.0, 1.0].

__repr__() method descriptor

Return repr(self).

address_fingerprint(text)

Serialize an address string into a deterministic key for deduplication and graph node identity.

The address is analyzed into classed tokens and each token is reduced to its most canonical form: numbers become plain digit strings (№17, 17. and 17 all key as 17), address keywords become their canonical short form (Boulevard and blvd. both key as blvd), unambiguous territory names in any supported language become their territory code (Syria, Syrian Arab Republic and Сирия all key as sy), and free text is transliterated where a narrow, systematic romanization exists. Token order is preserved: addresses that differ only by transposed numbers (д. 17 стр. 1 versus д. 1 стр. 17) must not key identically, so differently ordered renderings of one address also produce different keys — collapsing those is left to fuzzier machinery. Use this function to key or deduplicate address records; to score how similar two addresses are, use compare_address, which shares the same analysis (and its cache) but aligns tokens order-independently and fuzzily.

The output is lowercase, space-separated, and ASCII except for free-text tokens in scripts without a systematic romanization (Chinese, Arabic, ...), which pass through in their native script. Callers that require pure-ASCII identifiers should slug-encode the result (e.g. normality.slugify). Fingerprints are stable within one version of rigour but may change between versions as the underlying resources grow; do not persist them across upgrades without re-keying.

Parameters:

Name Type Description Default
text str

An address, as one full string.

required

Returns:

Type Description
str | None

The fingerprint string, or None when the input is empty or

str | None

contains nothing analyzable (only punctuation).

Source code in rigour/addresses/compare.py
def address_fingerprint(text: str) -> str | None:
    """Serialize an address string into a deterministic key for
    deduplication and graph node identity.

    The address is analyzed into classed tokens and each token is
    reduced to its most canonical form: numbers become plain digit
    strings (`№17`, `17.` and `17` all key as `17`), address
    keywords become their canonical short form (`Boulevard` and
    `blvd.` both key as `blvd`), unambiguous territory names in any
    supported language become their territory code (`Syria`, `Syrian
    Arab Republic` and `Сирия` all key as `sy`), and free text is
    transliterated where a narrow, systematic romanization exists.
    Token order is preserved: addresses that differ only by
    transposed numbers (`д. 17 стр. 1` versus `д. 1 стр. 17`) must
    not key identically, so differently ordered renderings of one
    address also produce different keys — collapsing those is left
    to fuzzier machinery. Use this function to key or deduplicate address
    records; to score how similar two addresses are, use
    [compare_address][rigour.addresses.compare.compare_address],
    which shares the same analysis (and its cache) but aligns tokens
    order-independently and fuzzily.

    The output is lowercase, space-separated, and ASCII except for
    free-text tokens in scripts without a systematic romanization
    (Chinese, Arabic, ...), which pass through in their native
    script. Callers that require pure-ASCII identifiers should
    slug-encode the result (e.g. `normality.slugify`). Fingerprints
    are stable within one version of rigour but may change between
    versions as the underlying resources grow; do not persist them
    across upgrades without re-keying.

    Args:
        text: An address, as one full string.

    Returns:
        The fingerprint string, or `None` when the input is empty or
        contains nothing analyzable (only punctuation).
    """
    return _address_fingerprint(text)

clean_address(full)

Remove common formatting errors from addresses.

Source code in rigour/addresses/cleaning.py
def clean_address(full: str) -> str:
    """Remove common formatting errors from addresses."""
    while True:
        full, count = REPL.subn(_sub_match, full)
        if count == 0:
            break
    return full.strip()

compare_address(query, result)

Compare two address strings, scoring how likely they denote the same place.

Both strings are analyzed into classed tokens (numbers, keyword signifiers like str./ул., territory names, free text) and greedily aligned: numbers must match exactly, keywords match across alias forms (boulevard/blvd), territory names match across languages via their code (Syria/Сирия), and free text matches by edit distance over transliterated forms. The score is the length-weighted share of aligned tokens, and a pair of numbers where each side asserts a value the other lacks (a differing house or unit number) is penalized far beyond its length. Comparison is order-independent, so differently arranged address parts do not lower the score.

Score semantics, measured on the labelled benchmark corpus in contrib/address_bench: equivalent renderings of one address score 1.0; transliterated or partly translated matches typically land between 0.5 and 0.9; an address and its less specific prefix (street dropped, city kept) around 0.6; pairs with conflicting house or unit numbers are pushed toward 0.0. The accuracy-optimal decision threshold on that corpus is ~0.3 — substantially lower than typical name-similarity calibrations.

Analysis results are memoized in a process-wide cache, so comparing one address against many candidates re-analyzes the repeated side only once. The GIL is released while comparing.

Parameters:

Name Type Description Default
query str

An address, as one full string.

required
result str

The address to compare against, as one full string.

required

Returns:

Type Description
float

A similarity score between 0.0 and 1.0. Empty or

float

punctuation-only input scores 0.0 against everything.

Source code in rigour/addresses/compare.py
def compare_address(query: str, result: str) -> float:
    """Compare two address strings, scoring how likely they denote
    the same place.

    Both strings are analyzed into classed tokens (numbers, keyword
    signifiers like `str.`/`ул.`, territory names, free text) and
    greedily aligned: numbers must match exactly, keywords match
    across alias forms (`boulevard`/`blvd`), territory names match
    across languages via their code (`Syria`/`Сирия`), and free text
    matches by edit distance over transliterated forms. The score is
    the length-weighted share of aligned tokens, and a pair of
    numbers where each side asserts a value the other lacks (a
    differing house or unit number) is penalized far beyond its
    length. Comparison is order-independent, so differently arranged
    address parts do not lower the score.

    Score semantics, measured on the labelled benchmark corpus in
    `contrib/address_bench`: equivalent renderings of one address
    score 1.0; transliterated or partly translated matches typically
    land between 0.5 and 0.9; an address and its less specific
    prefix (street dropped, city kept) around 0.6; pairs with
    conflicting house or unit numbers are pushed toward 0.0. The
    accuracy-optimal decision threshold on that corpus is ~0.3 —
    substantially lower than typical name-similarity calibrations.

    Analysis results are memoized in a process-wide cache, so
    comparing one address against many candidates re-analyzes the
    repeated side only once. The GIL is released while comparing.

    Args:
        query: An address, as one full string.
        result: The address to compare against, as one full string.

    Returns:
        A similarity score between 0.0 and 1.0. Empty or
        punctuation-only input scores 0.0 against everything.
    """
    return _compare_address(query, result)

compare_address_many(queries, results)

Compare two sets of address strings and return the best pairwise score.

The score-only form of match_addresses: same pairing, same analysis cache, without the match object.

Parameters:

Name Type Description Default
queries list[str]

Addresses of one entity, each as one full string.

required
results list[str]

Addresses of the other entity, each as one full string.

required

Returns:

Type Description
float

The highest pairwise similarity score between 0.0 and 1.0;

float

0.0 when either list is empty or nothing is comparable.

Source code in rigour/addresses/compare.py
def compare_address_many(queries: list[str], results: list[str]) -> float:
    """Compare two sets of address strings and return the best
    pairwise score.

    The score-only form of
    [match_addresses][rigour.addresses.compare.match_addresses]:
    same pairing, same analysis cache, without the match object.

    Args:
        queries: Addresses of one entity, each as one full string.
        results: Addresses of the other entity, each as one full
            string.

    Returns:
        The highest pairwise similarity score between 0.0 and 1.0;
        0.0 when either list is empty or nothing is comparable.
    """
    return _compare_address_many(queries, results)

format_address(address, country=None)

Format the given address part into a multi-line string that matches the conventions of the country of the given address.

Parameters:

Name Type Description Default
address dict[str, str | None]

The address parts to be combined. Common parts include: summary: A short description of the address. po_box: The PO box/mailbox number. street: The street or road name. house: The descriptive name of the house. house_number: The number of the house on the street. postal_code: The postal code or ZIP code. city: The city or town name. county: The county or district name. state: The state or province name. state_district: The state or province district name. state_code: The state or province code. country: The name of the country (words, not ISO code). country_code: A pre-normalized country code.

required
country str | None

ISO code for the country of the address.

None

Returns:

Type Description
str

A single-line string with the formatted address.

Source code in rigour/addresses/format.py
def format_address(
    address: dict[str, str | None], country: str | None = None
) -> str:
    """Format the given address part into a multi-line string that matches the
    conventions of the country of the given address.

    Args:
        address: The address parts to be combined. Common parts include:
            summary: A short description of the address.
            po_box: The PO box/mailbox number.
            street: The street or road name.
            house: The descriptive name of the house.
            house_number: The number of the house on the street.
            postal_code: The postal code or ZIP code.
            city: The city or town name.
            county: The county or district name.
            state: The state or province name.
            state_district: The state or province district name.
            state_code: The state or province code.
            country: The name of the country (words, not ISO code).
            country_code: A pre-normalized country code.
        country: ISO code for the country of the address.

    Returns:
        A single-line string with the formatted address.
    """
    text = _format(address, country=country)
    prev: str | None = None
    while prev != text:
        prev = text
        text = text.replace("\n\n", "\n").replace("\n ", "\n").strip()
    return text

format_address_line(address, country=None)

Format the given address part into a single-line string that matches the conventions of the country of the given address.

Parameters:

Name Type Description Default
address dict[str, str | None]

The address parts to be combined. Common parts include: summary: A short description of the address. po_box: The PO box/mailbox number. street: The street or road name. house: The descriptive name of the house. house_number: The number of the house on the street. postal_code: The postal code or ZIP code. city: The city or town name. county: The county or district name. state: The state or province name. state_district: The state or province district name. state_code: The state or province code. country: The name of the country (words, not ISO code). country_code: A pre-normalized country code.

required
country str | None

ISO code for the country of the address.

None

Returns:

Type Description
str

A single-line string with the formatted address.

Source code in rigour/addresses/format.py
def format_address_line(
    address: dict[str, str | None], country: str | None = None
) -> str:
    """Format the given address part into a single-line string that matches the
    conventions of the country of the given address.

    Args:
        address: The address parts to be combined. Common parts include:
            summary: A short description of the address.
            po_box: The PO box/mailbox number.
            street: The street or road name.
            house: The descriptive name of the house.
            house_number: The number of the house on the street.
            postal_code: The postal code or ZIP code.
            city: The city or town name.
            county: The county or district name.
            state: The state or province name.
            state_district: The state or province district name.
            state_code: The state or province code.
            country: The name of the country (words, not ISO code).
            country_code: A pre-normalized country code.
        country: ISO code for the country of the address.

    Returns:
        A single-line string with the formatted address.
    """
    line = ", ".join(_format(address, country=country).split("\n"))
    return clean_address(line)

match_addresses(queries, results)

Compare two sets of address strings and return the best pairwise match with the evidence behind it.

Scores every query against every result with compare_address and returns the winning pair as an AddressMatch: its score, the two input strings that produced it (query, result), and a one-line detail describing how the tokens aligned. Each distinct string is analyzed only once, so the list×list loop is substantially cheaper than the equivalent pairwise calls.

The detail line lists analyzed tokens (lowercased, narrowly transliterated) separated by spaces: aligned tokens first, in query order, then the leftovers of each side.

form meaning
berlin aligned, identical on both sides
boulevard~blvd aligned by edit distance, keyword alias or territory code (query left, result right)
-10115 only in the query address
+la only in the result address

A line without ~, - or + is an exact match; leftovers on one side only mark a subset relation; a -5 +7 pair of numbers is the penalized house- or unit-number conflict.

Parameters:

Name Type Description Default
queries list[str]

Addresses of one entity, each as one full string.

required
results list[str]

Addresses of the other entity, each as one full string.

required

Returns:

Type Description
AddressMatch | None

The best-scoring pair, or None when either list is empty or

AddressMatch | None

contains nothing analyzable (only punctuation). A pair with

AddressMatch | None

nothing in common is still returned, with a score of 0.0.

Source code in rigour/addresses/compare.py
def match_addresses(queries: list[str], results: list[str]) -> AddressMatch | None:
    """Compare two sets of address strings and return the best
    pairwise match with the evidence behind it.

    Scores every query against every result with
    [compare_address][rigour.addresses.compare.compare_address] and
    returns the winning pair as an
    [AddressMatch][rigour._core.AddressMatch]: its `score`, the two
    input strings that produced it (`query`, `result`), and a
    one-line `detail` describing how the tokens aligned. Each
    distinct string is analyzed only once, so the list×list loop is
    substantially cheaper than the equivalent pairwise calls.

    The `detail` line lists analyzed tokens (lowercased, narrowly
    transliterated) separated by spaces: aligned tokens first, in
    query order, then the leftovers of each side.

    | form | meaning |
    |---|---|
    | `berlin` | aligned, identical on both sides |
    | `boulevard~blvd` | aligned by edit distance, keyword alias or territory code (query left, result right) |
    | `-10115` | only in the query address |
    | `+la` | only in the result address |

    A line without `~`, `-` or `+` is an exact match; leftovers on
    one side only mark a subset relation; a `-5 +7` pair of numbers
    is the penalized house- or unit-number conflict.

    Args:
        queries: Addresses of one entity, each as one full string.
        results: Addresses of the other entity, each as one full
            string.

    Returns:
        The best-scoring pair, or `None` when either list is empty or
        contains nothing analyzable (only punctuation). A pair with
        nothing in common is still returned, with a score of 0.0.
    """
    return _match_addresses(queries, results)

normalize_address(address, latinize=False, min_length=4)

Build a comparison key from an address.

Casefolds, replaces punctuation/symbols with whitespace, tokenises on Unicode general-category, and rejoins with single-space separators. The output is a flat lowercase token sequence suitable for substring matching, equality keys, or feeding :func:shorten_address_keywords / :func:remove_address_keywordsnot a display form.

Parameters:

Name Type Description Default
address str

The address to normalise.

required
latinize bool

When True, transliterate non-ASCII tokens to ASCII via normality.ascii_text. Default False preserves the original script.

False
min_length int

Reject the result as None if it would be shorter than this many characters. Defaults to 4 to filter out single-token noise.

4

Returns:

Type Description
str | None

Normalised address, or None when the result is shorter

str | None

than min_length.

Source code in rigour/addresses/normalize.py
def normalize_address(
    address: str, latinize: bool = False, min_length: int = 4
) -> str | None:
    """Build a comparison key from an address.

    Casefolds, replaces punctuation/symbols with whitespace,
    tokenises on Unicode general-category, and rejoins with
    single-space separators. The output is a flat lowercase token
    sequence suitable for substring matching, equality keys, or
    feeding :func:`shorten_address_keywords` /
    :func:`remove_address_keywords` — **not** a display form.

    Args:
        address: The address to normalise.
        latinize: When `True`, transliterate non-ASCII tokens to
            ASCII via `normality.ascii_text`. Default `False`
            preserves the original script.
        min_length: Reject the result as `None` if it would be
            shorter than this many characters. Defaults to 4 to
            filter out single-token noise.

    Returns:
        Normalised address, or `None` when the result is shorter
        than `min_length`.
    """
    tokens: list[list[str]] = []
    token: list[str] = []
    for char in address.casefold():
        if char in CHARS_ALLOWED:
            chr: str | None = char
        else:
            cat = unicodedata.category(char)
            chr = TOKEN_SEP_CATEGORIES.get(cat, char)
        if chr is None:
            continue
        if chr == WS:
            if len(token):
                tokens.append(token)
            token = []
            continue
        token.append(chr)
    if len(token):
        tokens.append(token)

    parts: list[str] = []
    for token in tokens:
        token_str = "".join(token)
        if latinize:
            token_str = ascii_text(token_str)
        if len(token_str) == 0:
            continue
        parts.append(token_str)
    norm_address = WS.join(parts)
    if len(norm_address) < min_length:
        return None
    return norm_address

remove_address_keywords(address, latinize=False, replacement=WS)

Strip common address keywords from a normalised address.

Deprecated

Use compare_address to compare addresses instead of comparing keyword-stripped strings. This function will be removed in a future version.

Removes recognised forms ("street", "road", "south", territory names, ordinals, …) by substituting each match with replacement. Consecutive matches produce consecutive replacement runs — whitespace is not collapsed, so the output may contain multi-space gaps. Use normality.squash_spaces afterwards if a single-space output is wanted.

Input must already be normalised with :func:normalize_address using the same latinize flag — the alias table is built against that normalised form.

Parameters:

Name Type Description Default
address str

A pre-normalised address string.

required
latinize bool

Must match the flag passed to :func:normalize_address. Default False.

False
replacement str

String substituted in place of each match. Defaults to a single ASCII space.

WS

Returns:

Type Description
str | None

The address with recognised keywords removed.

Source code in rigour/addresses/normalize.py
def remove_address_keywords(
    address: str, latinize: bool = False, replacement: str = WS
) -> str | None:
    """Strip common address keywords from a normalised address.

    Deprecated:
        Use [compare_address][rigour.addresses.compare.compare_address]
        to compare addresses instead of comparing keyword-stripped
        strings. This function will be removed in a future version.

    Removes recognised forms (`"street"`, `"road"`, `"south"`,
    territory names, ordinals, …) by substituting each match with
    `replacement`. Consecutive matches produce consecutive
    `replacement` runs — whitespace is **not** collapsed, so the
    output may contain multi-space gaps. Use
    `normality.squash_spaces` afterwards if a single-space
    output is wanted.

    Input must already be normalised with :func:`normalize_address`
    using the same `latinize` flag — the alias table is built
    against that normalised form.

    Args:
        address: A pre-normalised address string.
        latinize: Must match the flag passed to
            :func:`normalize_address`. Default `False`.
        replacement: String substituted in place of each match.
            Defaults to a single ASCII space.

    Returns:
        The address with recognised keywords removed.
    """
    warnings.warn(
        "rigour.addresses.remove_address_keywords is deprecated, "
        "use rigour.addresses.compare_address instead",
        DeprecationWarning,
        stacklevel=2,
    )
    with resource_lock:
        pattern, _ = _address_replacer(latinize=latinize)
    return pattern.sub(replacement, address)

shorten_address_keywords(address, latinize=False)

Shorten common address keywords in a normalised address.

Deprecated

Use address_fingerprint, which reduces keywords to the same canonical short forms as part of a full keying serialization. This function will be removed in a future version.

Replaces recognised forms with their canonical short form ("street""st", "avenue""av", "united arab emirates""ae", …). Multi-token forms beat single-token components via longest-form-first ordering in the alias pattern, so country names win over their constituent words.

Input must already be normalised with :func:normalize_address using the same latinize flag — the alias table is built against that normalised form.

Parameters:

Name Type Description Default
address str

A pre-normalised address string.

required
latinize bool

Must match the flag passed to :func:normalize_address. Default False.

False

Returns:

Type Description
str | None

The address with recognised keywords shortened. Tokens

str | None

that don't match any alias pass through unchanged.

Source code in rigour/addresses/normalize.py
def shorten_address_keywords(address: str, latinize: bool = False) -> str | None:
    """Shorten common address keywords in a normalised address.

    Deprecated:
        Use [address_fingerprint][rigour.addresses.compare.address_fingerprint],
        which reduces keywords to the same canonical short forms as
        part of a full keying serialization. This function will be
        removed in a future version.

    Replaces recognised forms with their canonical short form
    (`"street"` → `"st"`, `"avenue"` → `"av"`, `"united arab
    emirates"` → `"ae"`, …). Multi-token forms beat single-token
    components via longest-form-first ordering in the alias
    pattern, so country names win over their constituent words.

    Input must already be normalised with :func:`normalize_address`
    using the same `latinize` flag — the alias table is built
    against that normalised form.

    Args:
        address: A pre-normalised address string.
        latinize: Must match the flag passed to
            :func:`normalize_address`. Default `False`.

    Returns:
        The address with recognised keywords shortened. Tokens
        that don't match any alias pass through unchanged.
    """
    warnings.warn(
        "rigour.addresses.shorten_address_keywords is deprecated, "
        "use rigour.addresses.address_fingerprint instead",
        DeprecationWarning,
        stacklevel=2,
    )
    pattern, mapping = _address_replacer(latinize=latinize)

    def _sub(match: re.Match[str]) -> str:
        value = match.group(1)
        return mapping.get(value.lower(), value)

    return pattern.sub(_sub, address)

rigour.addresses.compare

Compare postal address strings for referring to the same place.

AddressMatch

The best pair out of a list×list address comparison, with the evidence that produced it.

__doc__ = 'The best pair out of a list×list address comparison, with the\nevidence that produced it.' class-attribute

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.

__module__ = 'rigour._core' class-attribute

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.

detail property

One-line alignment summary over comparable token forms: aligned tokens in query order (berlin when identical, boulevard~blvd when aligned by edit distance or class equivalence), then -tok for query-only and +tok for result-only tokens.

query property

Query-side address of the winning pair, as supplied.

result property

Result-side address of the winning pair, as supplied.

score property

Similarity of the winning pair in [0.0, 1.0].

__repr__() method descriptor

Return repr(self).

address_fingerprint(text)

Serialize an address string into a deterministic key for deduplication and graph node identity.

The address is analyzed into classed tokens and each token is reduced to its most canonical form: numbers become plain digit strings (№17, 17. and 17 all key as 17), address keywords become their canonical short form (Boulevard and blvd. both key as blvd), unambiguous territory names in any supported language become their territory code (Syria, Syrian Arab Republic and Сирия all key as sy), and free text is transliterated where a narrow, systematic romanization exists. Token order is preserved: addresses that differ only by transposed numbers (д. 17 стр. 1 versus д. 1 стр. 17) must not key identically, so differently ordered renderings of one address also produce different keys — collapsing those is left to fuzzier machinery. Use this function to key or deduplicate address records; to score how similar two addresses are, use compare_address, which shares the same analysis (and its cache) but aligns tokens order-independently and fuzzily.

The output is lowercase, space-separated, and ASCII except for free-text tokens in scripts without a systematic romanization (Chinese, Arabic, ...), which pass through in their native script. Callers that require pure-ASCII identifiers should slug-encode the result (e.g. normality.slugify). Fingerprints are stable within one version of rigour but may change between versions as the underlying resources grow; do not persist them across upgrades without re-keying.

Parameters:

Name Type Description Default
text str

An address, as one full string.

required

Returns:

Type Description
str | None

The fingerprint string, or None when the input is empty or

str | None

contains nothing analyzable (only punctuation).

Source code in rigour/addresses/compare.py
def address_fingerprint(text: str) -> str | None:
    """Serialize an address string into a deterministic key for
    deduplication and graph node identity.

    The address is analyzed into classed tokens and each token is
    reduced to its most canonical form: numbers become plain digit
    strings (`№17`, `17.` and `17` all key as `17`), address
    keywords become their canonical short form (`Boulevard` and
    `blvd.` both key as `blvd`), unambiguous territory names in any
    supported language become their territory code (`Syria`, `Syrian
    Arab Republic` and `Сирия` all key as `sy`), and free text is
    transliterated where a narrow, systematic romanization exists.
    Token order is preserved: addresses that differ only by
    transposed numbers (`д. 17 стр. 1` versus `д. 1 стр. 17`) must
    not key identically, so differently ordered renderings of one
    address also produce different keys — collapsing those is left
    to fuzzier machinery. Use this function to key or deduplicate address
    records; to score how similar two addresses are, use
    [compare_address][rigour.addresses.compare.compare_address],
    which shares the same analysis (and its cache) but aligns tokens
    order-independently and fuzzily.

    The output is lowercase, space-separated, and ASCII except for
    free-text tokens in scripts without a systematic romanization
    (Chinese, Arabic, ...), which pass through in their native
    script. Callers that require pure-ASCII identifiers should
    slug-encode the result (e.g. `normality.slugify`). Fingerprints
    are stable within one version of rigour but may change between
    versions as the underlying resources grow; do not persist them
    across upgrades without re-keying.

    Args:
        text: An address, as one full string.

    Returns:
        The fingerprint string, or `None` when the input is empty or
        contains nothing analyzable (only punctuation).
    """
    return _address_fingerprint(text)

compare_address(query, result)

Compare two address strings, scoring how likely they denote the same place.

Both strings are analyzed into classed tokens (numbers, keyword signifiers like str./ул., territory names, free text) and greedily aligned: numbers must match exactly, keywords match across alias forms (boulevard/blvd), territory names match across languages via their code (Syria/Сирия), and free text matches by edit distance over transliterated forms. The score is the length-weighted share of aligned tokens, and a pair of numbers where each side asserts a value the other lacks (a differing house or unit number) is penalized far beyond its length. Comparison is order-independent, so differently arranged address parts do not lower the score.

Score semantics, measured on the labelled benchmark corpus in contrib/address_bench: equivalent renderings of one address score 1.0; transliterated or partly translated matches typically land between 0.5 and 0.9; an address and its less specific prefix (street dropped, city kept) around 0.6; pairs with conflicting house or unit numbers are pushed toward 0.0. The accuracy-optimal decision threshold on that corpus is ~0.3 — substantially lower than typical name-similarity calibrations.

Analysis results are memoized in a process-wide cache, so comparing one address against many candidates re-analyzes the repeated side only once. The GIL is released while comparing.

Parameters:

Name Type Description Default
query str

An address, as one full string.

required
result str

The address to compare against, as one full string.

required

Returns:

Type Description
float

A similarity score between 0.0 and 1.0. Empty or

float

punctuation-only input scores 0.0 against everything.

Source code in rigour/addresses/compare.py
def compare_address(query: str, result: str) -> float:
    """Compare two address strings, scoring how likely they denote
    the same place.

    Both strings are analyzed into classed tokens (numbers, keyword
    signifiers like `str.`/`ул.`, territory names, free text) and
    greedily aligned: numbers must match exactly, keywords match
    across alias forms (`boulevard`/`blvd`), territory names match
    across languages via their code (`Syria`/`Сирия`), and free text
    matches by edit distance over transliterated forms. The score is
    the length-weighted share of aligned tokens, and a pair of
    numbers where each side asserts a value the other lacks (a
    differing house or unit number) is penalized far beyond its
    length. Comparison is order-independent, so differently arranged
    address parts do not lower the score.

    Score semantics, measured on the labelled benchmark corpus in
    `contrib/address_bench`: equivalent renderings of one address
    score 1.0; transliterated or partly translated matches typically
    land between 0.5 and 0.9; an address and its less specific
    prefix (street dropped, city kept) around 0.6; pairs with
    conflicting house or unit numbers are pushed toward 0.0. The
    accuracy-optimal decision threshold on that corpus is ~0.3 —
    substantially lower than typical name-similarity calibrations.

    Analysis results are memoized in a process-wide cache, so
    comparing one address against many candidates re-analyzes the
    repeated side only once. The GIL is released while comparing.

    Args:
        query: An address, as one full string.
        result: The address to compare against, as one full string.

    Returns:
        A similarity score between 0.0 and 1.0. Empty or
        punctuation-only input scores 0.0 against everything.
    """
    return _compare_address(query, result)

compare_address_many(queries, results)

Compare two sets of address strings and return the best pairwise score.

The score-only form of match_addresses: same pairing, same analysis cache, without the match object.

Parameters:

Name Type Description Default
queries list[str]

Addresses of one entity, each as one full string.

required
results list[str]

Addresses of the other entity, each as one full string.

required

Returns:

Type Description
float

The highest pairwise similarity score between 0.0 and 1.0;

float

0.0 when either list is empty or nothing is comparable.

Source code in rigour/addresses/compare.py
def compare_address_many(queries: list[str], results: list[str]) -> float:
    """Compare two sets of address strings and return the best
    pairwise score.

    The score-only form of
    [match_addresses][rigour.addresses.compare.match_addresses]:
    same pairing, same analysis cache, without the match object.

    Args:
        queries: Addresses of one entity, each as one full string.
        results: Addresses of the other entity, each as one full
            string.

    Returns:
        The highest pairwise similarity score between 0.0 and 1.0;
        0.0 when either list is empty or nothing is comparable.
    """
    return _compare_address_many(queries, results)

match_addresses(queries, results)

Compare two sets of address strings and return the best pairwise match with the evidence behind it.

Scores every query against every result with compare_address and returns the winning pair as an AddressMatch: its score, the two input strings that produced it (query, result), and a one-line detail describing how the tokens aligned. Each distinct string is analyzed only once, so the list×list loop is substantially cheaper than the equivalent pairwise calls.

The detail line lists analyzed tokens (lowercased, narrowly transliterated) separated by spaces: aligned tokens first, in query order, then the leftovers of each side.

form meaning
berlin aligned, identical on both sides
boulevard~blvd aligned by edit distance, keyword alias or territory code (query left, result right)
-10115 only in the query address
+la only in the result address

A line without ~, - or + is an exact match; leftovers on one side only mark a subset relation; a -5 +7 pair of numbers is the penalized house- or unit-number conflict.

Parameters:

Name Type Description Default
queries list[str]

Addresses of one entity, each as one full string.

required
results list[str]

Addresses of the other entity, each as one full string.

required

Returns:

Type Description
AddressMatch | None

The best-scoring pair, or None when either list is empty or

AddressMatch | None

contains nothing analyzable (only punctuation). A pair with

AddressMatch | None

nothing in common is still returned, with a score of 0.0.

Source code in rigour/addresses/compare.py
def match_addresses(queries: list[str], results: list[str]) -> AddressMatch | None:
    """Compare two sets of address strings and return the best
    pairwise match with the evidence behind it.

    Scores every query against every result with
    [compare_address][rigour.addresses.compare.compare_address] and
    returns the winning pair as an
    [AddressMatch][rigour._core.AddressMatch]: its `score`, the two
    input strings that produced it (`query`, `result`), and a
    one-line `detail` describing how the tokens aligned. Each
    distinct string is analyzed only once, so the list×list loop is
    substantially cheaper than the equivalent pairwise calls.

    The `detail` line lists analyzed tokens (lowercased, narrowly
    transliterated) separated by spaces: aligned tokens first, in
    query order, then the leftovers of each side.

    | form | meaning |
    |---|---|
    | `berlin` | aligned, identical on both sides |
    | `boulevard~blvd` | aligned by edit distance, keyword alias or territory code (query left, result right) |
    | `-10115` | only in the query address |
    | `+la` | only in the result address |

    A line without `~`, `-` or `+` is an exact match; leftovers on
    one side only mark a subset relation; a `-5 +7` pair of numbers
    is the penalized house- or unit-number conflict.

    Args:
        queries: Addresses of one entity, each as one full string.
        results: Addresses of the other entity, each as one full
            string.

    Returns:
        The best-scoring pair, or `None` when either list is empty or
        contains nothing analyzable (only punctuation). A pair with
        nothing in common is still returned, with a score of 0.0.
    """
    return _match_addresses(queries, results)