fix(heuristics): unit number before a Saint-named city drops "St." from the location
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 85/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- typescript
- Domain
- backend
Research direction
Start in src/lib/heuristics/line-primitives.ts at the HOUSE_NUMBER_ONLY_RE constant (around line 763) and lastStreetTypeMatchEnd (around line 765). Widen the regex to allow an optional unit designator (Unit, Apt, Apt., Apartment, No., #) after the house number, then add the four new cases to the parameterized table in src/lib/heuristics/extract/contact.test.ts. Run npm run verify to confirm all tests pass and snapshots are unchanged.
Written by the indexing model from the issue text.
Description
Problem
The address-line locality extraction from #837 (PR #1126) drops St. from a Saint-named city when a unit number sits between the house number and the city, and there is no street name in between:
| Input | Today | Expected |
|---|---|---|
4567 Unit 12 St. Petersburg, FL 33701 |
Petersburg, FL |
St. Petersburg, FL |
4567 #12 St. Petersburg, FL 33701 |
Petersburg, FL |
St. Petersburg, FL |
Petersburg, FL is a wrong city, and it gets printed on the user's exported résumé. The module's own rule is "missing beats wrong" (extractLocalityFromAddressLine docblock), so the wrong value is the defect, not the miss.
Found by the bot review of PR #1126 at dfc3ad13. It was out of scope for that PR's final round; see the code-owner approval on #1126.
Decisions (2026-10-07)
Answers to the triage questions. Do not reopen them.
- Unblocked. PR #1126 merged as
15962c1and #837 is closed, so every anchor below is onmain. Branch frommain. - The target file is
src/lib/heuristics/line-primitives.ts. Onmain,HOUSE_NUMBER_ONLY_REis at:777and is used at:799insidelastStreetTypeMatchEnd;extractLocalityFromAddressLineis the function to test against.extract/contact.tscalls it throughlocationOnLineand needs no change. If line numbers drift, find the symbols by name.
Cause
lastStreetTypeMatchEnd (src/lib/heuristics/line-primitives.ts, around :765) treats a lone Saint-shaped St./Ste. as the start of the place name only when HOUSE_NUMBER_ONLY_RE matches everything before it:
const HOUSE_NUMBER_ONLY_RE = /^\s*\d+\s*$/;
The reasoning is that a street suffix closes a street name, so with only a house number in front there is no name to close. A unit designator is not a street name either, but the regex only accepts bare digits. So for 4567 Unit 12 St. Petersburg, the lone St. is trusted as the street suffix, and the cut lands after it.
4567 Suite 400 St. Petersburg, FL, 4567 Apt 4B St. Petersburg, FL and 4567 # 12 St. Petersburg, FL already return the right value on main (verified 2026-10-08): Suite, Apt and # followed by a space are themselves STREET_TYPE_RE matches, so those lines take the multi-match path. Only Unit (not a STREET_TYPE_RE word) and #12 with no space (the # branch needs trailing whitespace) reach the lone-match path and fail.
Fix
Widen HOUSE_NUMBER_ONLY_RE (and its doc comment) so the prefix can be a house number followed by an optional unit designator and its identifier:
- house number: digits, optionally with a single trailing letter (
4567,12B) - optional unit:
Unit,Apt,Apt.,Apartment,No.or#, case-insensitive, followed by an alphanumeric identifier (12,4B), with or without a space after#
Nothing else may be added to the prefix. Anything that could be a street name, such as 4567 Main St. Petersburg, must keep today's behaviour (trusted as the closer). Rename the constant if the new name reads better (for example HOUSE_NUMBER_PREFIX_RE); keep it module-private.
Acceptance criteria
-
extractLocalityFromAddressLine(s, true)returnsSt. Petersburg, FLfor4567 Unit 12 St. Petersburg, FL 33701and4567 #12 St. Petersburg, FL 33701(both fail onmaintoday; add them as fail-before cases). -
4567 Apt 4B St. Petersburg, FL 33701and4567 # 12 St. Petersburg, FL 33701still returnSt. Petersburg, FL(they pass today; pin them as regression cases). - These existing results are unchanged, and each is pinned by a test:
100 St. Petersburg, FL 33701→St. Petersburg, FL;4567 Main St, Springfield, IL 62704→Springfield, IL;4567 Main St Springfield, IL 62704→Springfield, IL;4567 Suite 400 St. Petersburg, FL 33701→St. Petersburg, FL;4567 Main Street St. Petersburg, FL 33701→St. Petersburg, FL. - New cases are added to the existing parameterized table in
src/lib/heuristics/extract/contact.test.ts(orline-primitivestests if they exist), not as one-offitblocks. -
corpus.test.tssnapshots are unchanged; no fixture or baseline is re-baked. -
npm run verifyis green.
Out of scope
-
4567 Main St. Petersburg, FL 33701staysPetersburg, FL(inherently ambiguous; a street name precedes theSt.). -
Unit designators after the street name (
4567 Main St Apt 2, Springfield, IL). That is the ordinary shape, and it already works. -
The other unguarded
US_LOCATION_RE/INTL_LOCATION_REcall sites (localize/contact.ts,experience-disambiguate.ts) noted in the #1126 review.
- Dominant language
- TypeScript
- Stars
- 11
- Forks
- 4
- PR merge metrics
- No merged PRs in 30d
Getting set up
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from offlinecv/OfflineCV
-
Migrate Gaal implement/review/revise to the local gaal watch; keep mechanical workflows on ActionsOpenarchitecture chore github_actions needs-human ready-for-agent
Difficulty 5/5 Over a week Newbie friendliness 8/100
-
Parser: achievement year lost on re-import when '· YYYY' wrapped to its own linePossibly taken @gaal-agent claimed this 8 days ago. Openbug gaal ready-for-agent ux:parsing
Difficulty 3/5 1-2 days Newbie friendliness 25/100
-
Export: auto-bolded metric bullets extract one word per line in stream-order parsers (pypdf/Drive)Possibly taken @gaal-agent claimed this 8 days ago. Openbatch:export-standards bug gaal ready-for-agent ux:edit-export
Difficulty 3/5 1-2 days Newbie friendliness 25/100
-
bug duplicate improvement
Difficulty 3/5 Half a day Newbie friendliness 66/100
-
Return legs from /download/ to /: Fix now lands on the missing field, and a bfcache miss keeps the résuméPossibly taken @s-annam claimed this 14 days ago. Openimprovement ready-for-agent ux:flow
All issues in offlinecv/OfflineCV
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 83/100
Maintainers usually reply within 1 day
-
Difficulty 1/5 1-3 hours Newbie friendliness 72/100
zts212653/clowder-ai#1678 ·
Maintainers usually reply within 1 day
-
good first issue
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
Progressive-Victory/the-website#680 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100