[decision] contact — location is a locality, not a mailing address; an address line must not yield a non-locality substring
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 68/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Quiet
- Tech stack
- typescript
- Domain
- backend, documentation, testing
Research direction
Start with src/lib/heuristics/extract/contact.ts and the US_LOCATION_RE definition in src/lib/heuristics/regex.ts, then read docs/canonical-resume-model.md and the fixture truth file. Run the contact tests and inspect the corpus scoreboard, especially phone-region behavior. Done means the address-shaped fixture no longer yields a street substring, locality behavior is preserved, documentation and truth data are updated, and the listed verification commands pass.
Written by the indexing model from the issue text.
Description
Summary
Part decision record, part defect. The contact location field has no written contract, and one corpus fixture shows the cost: a résumé that draws a full mailing address yields a location of "Main Street City, ST" — a string that is neither the mailing address nor a real locality. The decision (what location means) is cheap and should be written down; the residue it exposes (a non-locality substring lifted out of an address line) is a real precision defect and should be guarded, not accepted.
Context — why the entry exists
Each corpus fixture under tests/fixtures/pdfs/ may carry a hand-authored ground-truth sidecar (*.truth.json, minted under #654). Its knownWrong block records, per field, a place where the parser disagrees with what the page draws, with a status of open (live bug, issue must be open), accepted (a tradeoff someone WROTE DOWN — the baseline _readme is explicit that it is "never for silencing the script"), or unfiled (measured, never filed).
unfiled is capped at UNFILED_TRUTH_CEILING = 10 (src/lib/heuristics/corpus.test.ts:186) and is saturated at 10/10 today. This issue resolves one of them.
The disagreement
tests/fixtures/pdfs/word/chanchal-sharma-sample.pdf draws (pdftotext -layout):
Chanchal
Sharma
Office Manager
(718) 555–0100
[email protected]
4567 Main Street City, ST 98052
www.interestingsite.com
Observed: location = "Main Street City, ST".
Recorded today as a status: "unfiled" knownWrong entry on location in tests/fixtures/pdfs/word/chanchal-sharma-sample.truth.json, whose note reads: "The address loses its street number and its ZIP: '4567 Main Street City, ST 98052' comes back as 'Main Street City, ST'."
(This fixture is a Word résumé TEMPLATE, so City, ST is a literal placeholder rather than a real city — its provenance records that. The shape is what matters: <number> <street> <city>, <state> <zip>. A real résumé drawing 1600 Pennsylvania Ave Springfield, IL 62704 produces the same class of result: "Pennsylvania Ave Springfield, IL".)
Root cause
location is matched by a City, ST regex, applied to any line, with no notion of an address around it.
src/lib/heuristics/regex.ts:42-43:
export const US_LOCATION_RE =
/\b([A-Z][A-Za-z.\-]+(?:\s+[A-Z][A-Za-z.\-]+){0,2}),\s*([A-Z]{2})\b/;
Consumed by extractLocation (src/lib/heuristics/extract/contact.ts:283-293), which returns the first match in the profile band.
On the address line the regex starts at Main (the leading 4567 cannot begin a match — the pattern requires [A-Z] and has no digit branch), greedily takes the three allowed capitalized tokens Main Street City, then , ST. The \b after the state code ends the match, so the ZIP is outside it. Result: "Main Street City, ST".
The regex's own docblock (regex.ts:34-41) says the 3-token cap exists to stop it eating "prepositional context like 'of Engineering Seattle' out of column-merged Education lines" — i.e. the pattern is already known to be a substring matcher applied to lines it does not fully understand. This is the same failure mode on the contact band.
Two things to settle
1. The contract (decision, cheap)
location is a locality — city plus region/state, optionally country — not a mailing address. Everything downstream treats it that way:
regionFromLocation(location)(contact.ts:387) derives the libphonenumber parse region from it;- the Download PDF prints it under the name as a one-line locality;
- job search uses a locality, never a street.
That contract has never been written down. Write it into docs/canonical-resume-model.md next to the other contact-field definitions, and state the corollary explicitly: the parser does not extract street addresses, and a résumé that draws one should yield its locality or nothing — not a substring of the address.
Stating this also settles a live product question: the repo's PII posture (CLAUDE.md, hard rule on fixture PII) is much easier to hold if the canonical model has no field that can hold a home address.
2. The residue (defect, real)
Given that contract, "Main Street City, ST" is still wrong output. It is not the mailing address (by design) and it is not the locality (City, ST) — it is a junk string with a street name glued to the front, and it is what gets printed under the user's name on their exported PDF.
So this entry should NOT be flipped to accepted. accepted records a tradeoff someone chose; nobody would choose this string. Flip it to open against this issue and guard the shape.
Implementation plan
-
docs/canonical-resume-model.md— definelocation. One paragraph: a locality (city + region, optionally country), never a street address; citeUS_LOCATION_RE/INTL_LOCATION_REandextractLocationas the implementation. -
src/lib/heuristics/extract/contact.ts— detect the address shape and take the locality out of it. A mailing address has a reliable tell that a bare locality never has: a leading street number, and usually a trailing postal code./** A US street-address line: a leading house number, then the street, then the * locality, then a ZIP. `US_LOCATION_RE` is a substring matcher, so on such a * line it captures the street name as part of the city ("Main Street City, ST"). * Detected here so the locality is taken from the address's own tail rather * than from wherever the greedy 3-token run happens to start. */ const US_STREET_ADDRESS_RE = /^\s*\d+\s+\S/;When the candidate line matches, re-run the locality match against the segment that PRECEDES the postal code and FOLLOWS the street, or — simpler and less fragile — anchor the locality match to the end of the line (before an optional ZIP) instead of taking the first match:
const US_LOCATION_TAIL_RE = /([A-Z][A-Za-z.\-]+(?:\s+[A-Z][A-Za-z.\-]+){0,2}),\s*([A-Z]{2})(?:\s+\d{5}(?:-\d{4})?)?\s*$/;That still over-captures on
Main Street Cityfor this fixture, because the street name is capitalized and adjacent — so bound it further: on an address-shaped line, take only the tokens after the street-type word (Street,St,Ave,Avenue,Road,Rd,Blvd,Lane,Ln,Drive,Dr,Court,Ct,Way,Place,Pl,Terrace,Suite,Apt,#). For4567 Main Street City, ST 98052that leavesCity, ST.If that cannot be made reliable, prefer returning nothing over returning junk on an address-shaped line. A missing
locationcosts a small amount of score completeness; a wrong one is printed on the user's exported résumé. Whichever way it lands, state the choice in the code comment. -
Do not store the street address anywhere, including as a discarded intermediate that could reach a snapshot, a debug dump, or
rawTextconsumers. The contract in step 1 is the reason. -
Flip the ground-truth entry in
tests/fixtures/pdfs/word/chanchal-sharma-sample.truth.json→location, tostatus: "open"with this issue's number; delete it if the guard lands in the same PR. Do not edit the truth VALUE — a truth file records what the page draws (see itsprovenance), so the recorded value stays the full drawn address regardless of what the parser is taught to return; the truth's expectation forlocationis then whatever step 1's contract says it should be, recorded per that rule. -
Lower
UNFILED_TRUTH_CEILING(src/lib/heuristics/corpus.test.ts:186) by 1. Sibling issues from the same audit also lower it — on a rebase conflict, take the LOWER number.
Acceptance criteria
-
docs/canonical-resume-model.mddefineslocationas a locality and states that street addresses are out of the model. -
chanchal-sharma-sample.pdfparseslocationas"City, ST"— or as absent, if the code comment records that choice. It does NOT parse as"Main Street City, ST". - A real address shape is covered by a unit test:
1600 Pennsylvania Ave Springfield, IL 62704→"Springfield, IL"(or absent). Test insrc/lib/heuristics/extract/contact.test.ts. - A plain locality line is unchanged:
Chicago, IL→"Chicago, IL";San Francisco, CA→"San Francisco, CA";Bengaluru, India→ unchanged on the international path. - A locality with a suite/unit prefix does not regress:
Suite 400, Austin, TXyields"Austin, TX"or absent, never"Suite 400, Austin". -
regionFromLocationstill derives the correct phone region on every corpus fixture —locationfeeds it (contact.ts:387), so a changed or newly-absent location must not silently reroute phone parsing. Check thecontact.phonecolumn of the truth scoreboard specifically. -
npx vitest run src/lib/heuristics/corpus.test.tspasses withlocationprecision improved and recall unchanged on the other 57 fixtures. -
npx vitest run src/lib/heuristics/corpus-roundtrip.test.tspasses with no newKNOWN_FAILURESbaseline rows. -
npm run check:baselinesreports 1 fewer unfiled entry. -
npm run verifypasses.
- Dominant language
- TypeScript
- Stars
- 11
- Forks
- 4
- Avg merge
- 18h 56m
- Merged PRs (30d)
- 92
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
-
documentation
Difficulty 1/5 Under an hour Newbie friendliness 92/100
Maintainers usually reply within 1 day
-
documentation
Difficulty 1/5 Under an hour Newbie friendliness 92/100
Maintainers usually reply within 1 day
-
bug gaal ready-for-agent
Difficulty 4/5 3-5 days Newbie friendliness 58/100
Maintainers usually reply within 1 day
-
bug gaal refactor
Difficulty 4/5 3-5 days Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
[epic] Fix It guidance: advice that is right for the résumé in front of the userPossibly taken @Samhit21 claimed this 1 day ago. Openimprovement ready-for-agent ux:score-clarity
offlinecv/OfflineCV#1086 · 1 comment · 1 assignee ·
Maintainers usually reply within 1 day
All issues in offlinecv/OfflineCV
Similar issues
-
priority: P2
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
prime-radiant-inc/evener#3291 ·
Maintainers usually reply within 1 day
-
accessibility bug revealjs
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
quarto-dev/quarto-cli#14961 ·
Maintainers usually reply within 1 day
-
Difficulty 1/5 Under an hour Newbie friendliness 90/100
supabase/agent-skills#614 ·
-
Content
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
RunestoneInteractive/rs#1559 · 1 comment ·
Maintainers usually reply within 2 days