FA-96121 / Postal address normalization / Open access
Ordinal street name normalizer: suffix lookup uses the whole number · case 01
"Twenty-First" becomes "21TH".
ROOT CAUSE
The ST/ND/RD table is indexed with n rather than its final digit.
VERIFIED REPAIR
Index the table with n % 10.
Unsuccessful approach: Indexing with n % 100 still misses 21, 22 and 23.
Case contract
Uppercase, turn hyphens into spaces and split. A tens word (TWENTY, THIRTY, FORTY) immediately followed by a unit ordinal (FIRST..NINTH) forms one number; unit ordinals and TENTH..THIRTEENTH stand alone; digit tokens ending in ST/ND/RD/TH are re-suffixed. Each number is written with the suffix TH for values ending 11-13 (mod 100), else ST/ND/RD for final digits 1/2/3, else TH. Other tokens pass through.
Why this case matters
"Twenty-First Ave" and "21st Ave" must produce the same key or two addresses for one building never match.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(x):
UNITS = {'FIRST': 1, 'SECOND': 2, 'THIRD': 3, 'FOURTH': 4, 'FIFTH': 5, 'SIXTH': 6, 'SEVENTH': 7,
'EIGHTH': 8, 'NINTH': 9}
TEENS = {'TENTH': 10, 'ELEVENTH': 11, 'TWELFTH': 12, 'THIRTEENTH': 13}
TENS = {'TWENTY': 20, 'THIRTY': 30, 'FORTY': 40}
def suffix(n):
if n % 100 in (11, 12, 13):
return 'TH'
return {1: 'ST', 2: 'ND', 3: 'RD'}.get(n, 'TH')
t = x.upper().replace('-', ' ').split()
out, i = [], 0
while i < len(t):
w = t[i]
if w in TENS and i + 1 < len(t) and t[i + 1] in UNITS:
n = TENS[w] + UNITS[t[i + 1]]
i += 2
elif w in UNITS or w in TEENS:
n = UNITS.get(w) or TEENS[w]
i += 1
elif w[:-2].isdigit() and w[-2:] in ('ST', 'ND', 'RD', 'TH'):
n = int(w[:-2])
i += 1
else:
out.append(w)
i += 1
continue
out.append(str(n) + suffix(n))
return ' '.join(out)
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['hyphenated compound ordinal', 'Twenty-First AVE', '21ST AVE'], ['spaced compound ordinal', 'THIRTY SECOND AVE', '32ND AVE'], ['spaced compound followed by suffix', 'TWENTY THIRD AVE NORTH', '23RD AVE NORTH'], ['eleventh', 'ELEVENTH AVE', '11TH AVE'], ['thirteenth', 'THIRTEENTH AVE', '13TH AVE'], ['three-digit teen ordinal', '111TH AVE', '111TH AVE'], ['numeric ordinal with wrong suffix', '2ST AVE', '2ND AVE'], ['numeric ordinal with another wrong suffix', '43ND AVE', '43RD AVE'], ['simple ordinal', 'THIRD AVE', '3RD AVE'], ['tens word not followed by a unit is a name', 'FORTY OAKS AVE', 'FORTY OAKS AVE']], [['hyphenated compound ordinal', 'Twenty-First ST', '21ST ST'], ['spaced compound ordinal', 'THIRTY SECOND ST', '32ND ST'], ['spaced compound followed by suffix', 'TWENTY THIRD ST NORTH', '23RD ST NORTH'], ['eleventh', 'ELEVENTH ST', '11TH ST'], ['thirteenth', 'THIRTEENTH ST', '13TH ST'], ['three-digit teen ordinal', '211TH ST', '211TH ST'], ['numeric ordinal with wrong suffix', '2ST ST', '2ND ST'], ['numeric ordinal with another wrong suffix', '43ND ST', '43RD ST'], ['simple ordinal', 'THIRD ST', '3RD ST'], ['tens word not followed by a unit is a name', 'FORTY OAKS ST', 'FORTY OAKS ST']], [['hyphenated compound ordinal', 'Twenty-First PL', '21ST PL'], ['spaced compound ordinal', 'THIRTY SECOND PL', '32ND PL'], ['spaced compound followed by suffix', 'TWENTY THIRD PL NORTH', '23RD PL NORTH'], ['eleventh', 'ELEVENTH PL', '11TH PL'], ['thirteenth', 'THIRTEENTH PL', '13TH PL'], ['three-digit teen ordinal', '311TH PL', '311TH PL'], ['numeric ordinal with wrong suffix', '2ST PL', '2ND PL'], ['numeric ordinal with another wrong suffix', '43ND PL', '43RD PL'], ['simple ordinal', 'THIRD PL', '3RD PL'], ['tens word not followed by a unit is a name', 'FORTY OAKS PL', 'FORTY OAKS PL']], [['hyphenated compound ordinal', 'Twenty-First CT', '21ST CT'], ['spaced compound ordinal', 'THIRTY SECOND CT', '32ND CT'], ['spaced compound followed by suffix', 'TWENTY THIRD CT NORTH', '23RD CT NORTH'], ['eleventh', 'ELEVENTH CT', '11TH CT'], ['thirteenth', 'THIRTEENTH CT', '13TH CT'], ['three-digit teen ordinal', '411TH CT', '411TH CT'], ['numeric ordinal with wrong suffix', '2ST CT', '2ND CT'], ['numeric ordinal with another wrong suffix', '43ND CT', '43RD CT'], ['simple ordinal', 'THIRD CT', '3RD CT'], ['tens word not followed by a unit is a name', 'FORTY OAKS CT', 'FORTY OAKS CT']], [['hyphenated compound ordinal', 'Twenty-First LN', '21ST LN'], ['spaced compound ordinal', 'THIRTY SECOND LN', '32ND LN'], ['spaced compound followed by suffix', 'TWENTY THIRD LN NORTH', '23RD LN NORTH'], ['eleventh', 'ELEVENTH LN', '11TH LN'], ['thirteenth', 'THIRTEENTH LN', '13TH LN'], ['three-digit teen ordinal', '511TH LN', '511TH LN'], ['numeric ordinal with wrong suffix', '2ST LN', '2ND LN'], ['numeric ordinal with another wrong suffix', '43ND LN', '43RD LN'], ['simple ordinal', 'THIRD LN', '3RD LN'], ['tens word not followed by a unit is a name', 'FORTY OAKS LN', 'FORTY OAKS LN']]]
for label, arg, expected in fixtures[N - 1]:
check(label, solve(arg), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| hyphenated compound ordinal | 21TH AVE | 21ST AVE | Failed |
| spaced compound ordinal | 32TH AVE | 32ND AVE | Failed |
| spaced compound followed by suffix | 23TH AVE NORTH | 23RD AVE NORTH | Failed |
| eleventh | 11TH AVE | 11TH AVE | Passed |
| thirteenth | 13TH AVE | 13TH AVE | Passed |
| three-digit teen ordinal | 111TH AVE | 111TH AVE | Passed |
| numeric ordinal with wrong suffix | 2ND AVE | 2ND AVE | Passed |
| numeric ordinal with another wrong suffix | 43TH AVE | 43RD AVE | Failed |
| simple ordinal | 3RD AVE | 3RD AVE | Passed |
| tens word not followed by a unit is a name | FORTY OAKS AVE | FORTY OAKS AVE | Passed |
SHA-256 / a6d7bf57405d489479221085365e8c6aa40411af9414c8a520d3ad68ac694512
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(x):
UNITS = {'FIRST': 1, 'SECOND': 2, 'THIRD': 3, 'FOURTH': 4, 'FIFTH': 5, 'SIXTH': 6, 'SEVENTH': 7,
'EIGHTH': 8, 'NINTH': 9}
TEENS = {'TENTH': 10, 'ELEVENTH': 11, 'TWELFTH': 12, 'THIRTEENTH': 13}
TENS = {'TWENTY': 20, 'THIRTY': 30, 'FORTY': 40}
def suffix(n):
if n % 100 in (11, 12, 13):
return 'TH'
return {1: 'ST', 2: 'ND', 3: 'RD'}.get(n % 100, 'TH')
t = x.upper().replace('-', ' ').split()
out, i = [], 0
while i < len(t):
w = t[i]
if w in TENS and i + 1 < len(t) and t[i + 1] in UNITS:
n = TENS[w] + UNITS[t[i + 1]]
i += 2
elif w in UNITS or w in TEENS:
n = UNITS.get(w) or TEENS[w]
i += 1
elif w[:-2].isdigit() and w[-2:] in ('ST', 'ND', 'RD', 'TH'):
n = int(w[:-2])
i += 1
else:
out.append(w)
i += 1
continue
out.append(str(n) + suffix(n))
return ' '.join(out)
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['hyphenated compound ordinal', 'Twenty-First AVE', '21ST AVE'], ['spaced compound ordinal', 'THIRTY SECOND AVE', '32ND AVE'], ['spaced compound followed by suffix', 'TWENTY THIRD AVE NORTH', '23RD AVE NORTH'], ['eleventh', 'ELEVENTH AVE', '11TH AVE'], ['thirteenth', 'THIRTEENTH AVE', '13TH AVE'], ['three-digit teen ordinal', '111TH AVE', '111TH AVE'], ['numeric ordinal with wrong suffix', '2ST AVE', '2ND AVE'], ['numeric ordinal with another wrong suffix', '43ND AVE', '43RD AVE'], ['simple ordinal', 'THIRD AVE', '3RD AVE'], ['tens word not followed by a unit is a name', 'FORTY OAKS AVE', 'FORTY OAKS AVE']], [['hyphenated compound ordinal', 'Twenty-First ST', '21ST ST'], ['spaced compound ordinal', 'THIRTY SECOND ST', '32ND ST'], ['spaced compound followed by suffix', 'TWENTY THIRD ST NORTH', '23RD ST NORTH'], ['eleventh', 'ELEVENTH ST', '11TH ST'], ['thirteenth', 'THIRTEENTH ST', '13TH ST'], ['three-digit teen ordinal', '211TH ST', '211TH ST'], ['numeric ordinal with wrong suffix', '2ST ST', '2ND ST'], ['numeric ordinal with another wrong suffix', '43ND ST', '43RD ST'], ['simple ordinal', 'THIRD ST', '3RD ST'], ['tens word not followed by a unit is a name', 'FORTY OAKS ST', 'FORTY OAKS ST']], [['hyphenated compound ordinal', 'Twenty-First PL', '21ST PL'], ['spaced compound ordinal', 'THIRTY SECOND PL', '32ND PL'], ['spaced compound followed by suffix', 'TWENTY THIRD PL NORTH', '23RD PL NORTH'], ['eleventh', 'ELEVENTH PL', '11TH PL'], ['thirteenth', 'THIRTEENTH PL', '13TH PL'], ['three-digit teen ordinal', '311TH PL', '311TH PL'], ['numeric ordinal with wrong suffix', '2ST PL', '2ND PL'], ['numeric ordinal with another wrong suffix', '43ND PL', '43RD PL'], ['simple ordinal', 'THIRD PL', '3RD PL'], ['tens word not followed by a unit is a name', 'FORTY OAKS PL', 'FORTY OAKS PL']], [['hyphenated compound ordinal', 'Twenty-First CT', '21ST CT'], ['spaced compound ordinal', 'THIRTY SECOND CT', '32ND CT'], ['spaced compound followed by suffix', 'TWENTY THIRD CT NORTH', '23RD CT NORTH'], ['eleventh', 'ELEVENTH CT', '11TH CT'], ['thirteenth', 'THIRTEENTH CT', '13TH CT'], ['three-digit teen ordinal', '411TH CT', '411TH CT'], ['numeric ordinal with wrong suffix', '2ST CT', '2ND CT'], ['numeric ordinal with another wrong suffix', '43ND CT', '43RD CT'], ['simple ordinal', 'THIRD CT', '3RD CT'], ['tens word not followed by a unit is a name', 'FORTY OAKS CT', 'FORTY OAKS CT']], [['hyphenated compound ordinal', 'Twenty-First LN', '21ST LN'], ['spaced compound ordinal', 'THIRTY SECOND LN', '32ND LN'], ['spaced compound followed by suffix', 'TWENTY THIRD LN NORTH', '23RD LN NORTH'], ['eleventh', 'ELEVENTH LN', '11TH LN'], ['thirteenth', 'THIRTEENTH LN', '13TH LN'], ['three-digit teen ordinal', '511TH LN', '511TH LN'], ['numeric ordinal with wrong suffix', '2ST LN', '2ND LN'], ['numeric ordinal with another wrong suffix', '43ND LN', '43RD LN'], ['simple ordinal', 'THIRD LN', '3RD LN'], ['tens word not followed by a unit is a name', 'FORTY OAKS LN', 'FORTY OAKS LN']]]
for label, arg, expected in fixtures[N - 1]:
check(label, solve(arg), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| hyphenated compound ordinal | 21TH AVE | 21ST AVE | Failed |
| spaced compound ordinal | 32TH AVE | 32ND AVE | Failed |
| spaced compound followed by suffix | 23TH AVE NORTH | 23RD AVE NORTH | Failed |
| eleventh | 11TH AVE | 11TH AVE | Passed |
| thirteenth | 13TH AVE | 13TH AVE | Passed |
| three-digit teen ordinal | 111TH AVE | 111TH AVE | Passed |
| numeric ordinal with wrong suffix | 2ND AVE | 2ND AVE | Passed |
| numeric ordinal with another wrong suffix | 43TH AVE | 43RD AVE | Failed |
| simple ordinal | 3RD AVE | 3RD AVE | Passed |
| tens word not followed by a unit is a name | FORTY OAKS AVE | FORTY OAKS AVE | Passed |
SHA-256 / 8eae066ff7a67d0b3bc7123ee6f7190883651726799f164b8434253948acf77f
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(x):
UNITS = {'FIRST': 1, 'SECOND': 2, 'THIRD': 3, 'FOURTH': 4, 'FIFTH': 5, 'SIXTH': 6, 'SEVENTH': 7,
'EIGHTH': 8, 'NINTH': 9}
TEENS = {'TENTH': 10, 'ELEVENTH': 11, 'TWELFTH': 12, 'THIRTEENTH': 13}
TENS = {'TWENTY': 20, 'THIRTY': 30, 'FORTY': 40}
def suffix(n):
if n % 100 in (11, 12, 13):
return 'TH'
return {1: 'ST', 2: 'ND', 3: 'RD'}.get(n % 10, 'TH')
t = x.upper().replace('-', ' ').split()
out, i = [], 0
while i < len(t):
w = t[i]
if w in TENS and i + 1 < len(t) and t[i + 1] in UNITS:
n = TENS[w] + UNITS[t[i + 1]]
i += 2
elif w in UNITS or w in TEENS:
n = UNITS.get(w) or TEENS[w]
i += 1
elif w[:-2].isdigit() and w[-2:] in ('ST', 'ND', 'RD', 'TH'):
n = int(w[:-2])
i += 1
else:
out.append(w)
i += 1
continue
out.append(str(n) + suffix(n))
return ' '.join(out)
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['hyphenated compound ordinal', 'Twenty-First AVE', '21ST AVE'], ['spaced compound ordinal', 'THIRTY SECOND AVE', '32ND AVE'], ['spaced compound followed by suffix', 'TWENTY THIRD AVE NORTH', '23RD AVE NORTH'], ['eleventh', 'ELEVENTH AVE', '11TH AVE'], ['thirteenth', 'THIRTEENTH AVE', '13TH AVE'], ['three-digit teen ordinal', '111TH AVE', '111TH AVE'], ['numeric ordinal with wrong suffix', '2ST AVE', '2ND AVE'], ['numeric ordinal with another wrong suffix', '43ND AVE', '43RD AVE'], ['simple ordinal', 'THIRD AVE', '3RD AVE'], ['tens word not followed by a unit is a name', 'FORTY OAKS AVE', 'FORTY OAKS AVE']], [['hyphenated compound ordinal', 'Twenty-First ST', '21ST ST'], ['spaced compound ordinal', 'THIRTY SECOND ST', '32ND ST'], ['spaced compound followed by suffix', 'TWENTY THIRD ST NORTH', '23RD ST NORTH'], ['eleventh', 'ELEVENTH ST', '11TH ST'], ['thirteenth', 'THIRTEENTH ST', '13TH ST'], ['three-digit teen ordinal', '211TH ST', '211TH ST'], ['numeric ordinal with wrong suffix', '2ST ST', '2ND ST'], ['numeric ordinal with another wrong suffix', '43ND ST', '43RD ST'], ['simple ordinal', 'THIRD ST', '3RD ST'], ['tens word not followed by a unit is a name', 'FORTY OAKS ST', 'FORTY OAKS ST']], [['hyphenated compound ordinal', 'Twenty-First PL', '21ST PL'], ['spaced compound ordinal', 'THIRTY SECOND PL', '32ND PL'], ['spaced compound followed by suffix', 'TWENTY THIRD PL NORTH', '23RD PL NORTH'], ['eleventh', 'ELEVENTH PL', '11TH PL'], ['thirteenth', 'THIRTEENTH PL', '13TH PL'], ['three-digit teen ordinal', '311TH PL', '311TH PL'], ['numeric ordinal with wrong suffix', '2ST PL', '2ND PL'], ['numeric ordinal with another wrong suffix', '43ND PL', '43RD PL'], ['simple ordinal', 'THIRD PL', '3RD PL'], ['tens word not followed by a unit is a name', 'FORTY OAKS PL', 'FORTY OAKS PL']], [['hyphenated compound ordinal', 'Twenty-First CT', '21ST CT'], ['spaced compound ordinal', 'THIRTY SECOND CT', '32ND CT'], ['spaced compound followed by suffix', 'TWENTY THIRD CT NORTH', '23RD CT NORTH'], ['eleventh', 'ELEVENTH CT', '11TH CT'], ['thirteenth', 'THIRTEENTH CT', '13TH CT'], ['three-digit teen ordinal', '411TH CT', '411TH CT'], ['numeric ordinal with wrong suffix', '2ST CT', '2ND CT'], ['numeric ordinal with another wrong suffix', '43ND CT', '43RD CT'], ['simple ordinal', 'THIRD CT', '3RD CT'], ['tens word not followed by a unit is a name', 'FORTY OAKS CT', 'FORTY OAKS CT']], [['hyphenated compound ordinal', 'Twenty-First LN', '21ST LN'], ['spaced compound ordinal', 'THIRTY SECOND LN', '32ND LN'], ['spaced compound followed by suffix', 'TWENTY THIRD LN NORTH', '23RD LN NORTH'], ['eleventh', 'ELEVENTH LN', '11TH LN'], ['thirteenth', 'THIRTEENTH LN', '13TH LN'], ['three-digit teen ordinal', '511TH LN', '511TH LN'], ['numeric ordinal with wrong suffix', '2ST LN', '2ND LN'], ['numeric ordinal with another wrong suffix', '43ND LN', '43RD LN'], ['simple ordinal', 'THIRD LN', '3RD LN'], ['tens word not followed by a unit is a name', 'FORTY OAKS LN', 'FORTY OAKS LN']]]
for label, arg, expected in fixtures[N - 1]:
check(label, solve(arg), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| hyphenated compound ordinal | 21ST AVE | 21ST AVE | Passed |
| spaced compound ordinal | 32ND AVE | 32ND AVE | Passed |
| spaced compound followed by suffix | 23RD AVE NORTH | 23RD AVE NORTH | Passed |
| eleventh | 11TH AVE | 11TH AVE | Passed |
| thirteenth | 13TH AVE | 13TH AVE | Passed |
| three-digit teen ordinal | 111TH AVE | 111TH AVE | Passed |
| numeric ordinal with wrong suffix | 2ND AVE | 2ND AVE | Passed |
| numeric ordinal with another wrong suffix | 43RD AVE | 43RD AVE | Passed |
| simple ordinal | 3RD AVE | 3RD AVE | Passed |
| tens word not followed by a unit is a name | FORTY OAKS AVE | FORTY OAKS AVE | Passed |
SHA-256 / a6ccec3922b4c3ec9439f3274f48425c134f5c9d504c800b9e455025a7b9921b
Verification & scope
A deterministic toy contract over synthetic, obviously fictitious addresses. The rules are stipulated for this model only and make no claim of conformance with any postal authority standard. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.
Observations recorded using Python 3.12.14 at 2026-09-29T14:52:19.971669+00:00.
Case digest / 7d175e006e5228423fb801a0e958bfeffa366cd8d89ec9d7a08e1faf829beed7