Skip to content

feat(matchers): match by phone number and email address - #850

Merged
brawer merged 3 commits into
mainfrom
contact-matching-main
Oct 25, 2026
Merged

brawer merged 3 commits into
mainfrom
contact-matching-main

Conversation

@brawer

@brawer brawer commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Requested by Sascha · project thread

What changes

PoiMatcher can now match features that have no brand:wikidata, by a shared phone number or email address.

  • Contact evidence. If the ATP feature and the OSM candidate share a descriptive phone or email, the score is 0.98 × category factor × distance score. The category factor is 1.0 for equal main categories, 0.95 for a pair in src/matchers/contact/category_pairs.tsv, and 0 otherwise. Values in the nondescript sets (feat: compute denylist of ATP switchboard phone numbers #844–feat: compute nondescript email addresses from AllThePlaces #846) never count. A brand:wikidata conflict still vetoes the match. Beyond 150 m, the OSM street and house number must also appear in the ATP address (src/matchers/address.rs).
  • Category pairs. scripts/infer_contact_category_pairs.py suggested 61 pairs from a planet run. I removed 18 by hand, mostly shop-in-shop cases like Western Union counters in supermarkets, post offices in convenience stores, and hospital departments. They stay in the file as comments, with the reason.
  • Wider categories. Restaurants, lodging, schools and fuel stations now get a matcher, not only shops. Cafes, fast food, bars, schools, colleges, clinics, doctors, car rental, cinemas, money transfer, vets, fitness and sports centres, museums and healthcare=* get a match mask.
  • Parsers. The phone and email parsers move from pipeline::atp::nondescript to matchers::contact, so both sides parse alike. OSM numbers that are not in + form go through the phonenumber crate, in the ATP feature's addr:country.
  • Conflate opens the two nondescript sets, and reruns when either of them changes.
  • Docs. The background note is docs/notes/2026-10-25-phone-and-email-matching.md. TECHNICAL_DESIGN.md and scripts/README.md are updated too.

Results

These planet numbers come from the experimental branch contact-matching:

  • Wider categories. They raise brand:wikidata matches from 825,901 to 1,233,452. Most of the gain is fast food and fuel stations. For charging stations, the median ATP–OSM distance is 8 m.
  • Precision. With brand tags hidden on both sides, contact evidence alone finds the feature that brand:wikidata found in 97.1% of about 183k cases. Hand checks of new matches put precision at about 97%.
  • Lost matches. No existing match was lost.
  • Phone parsing cost. Parsing only the non-+ numbers with the crate costs about 15 CPU-seconds on the planet. In India, Thailand, Romania, Japan and Korea, 15–38% of OSM numbers are in national format.

A planet rerun of this PR's own code matched 1,302,469 ATP rows. That is 69,017 more than brand:wikidata with the wider categories alone, with none lost and 535 moved. The match step took 38 s. The experimental branch matched about 34k more, all of them from its brand-tag matching, which this PR leaves out.

The brand-tag matching from matching-experiments is not part of this PR.

Tests

  • cargo fmt, cargo clippy --all-targets -- -D warnings
  • cargo test: all unit and integration tests pass. I installed tippecanoe locally for the integration tests.
  • New tests: matchers::contact (category factor, OSM phone parsing, nondescript filtering, agreement) and test_poi_matcher_contact (pair, veto, national-format number, no matcher without evidence).
  • uv run pytest test_infer_contact_category_pairs.py passes. The test-on-hetzner tests fail locally with and without this change.
  • cargo deny check passes. deny.toml now ignores RUSTSEC-2023-0089 (atomic-polyfill is unmaintained). phonenumber pulls it in through postcard and heapless 0.7, but only for targets without native atomics.

Generated by Claude Code

@github-actions github-actions Bot added the enhancement New feature or request label Oct 25, 2026

brawer commented Oct 25, 2026

Copy link
Copy Markdown
Owner Author

The advisories check failed for two reasons:

  • RUSTSEC-2026-0386 (tokio 1.53.1). This one is not caused by this PR, since it fails on main too. I ported the fix from fix(deps): bump tokio to 1.54.1 for RUSTSEC-2026-0386 #849 here, a lockfile bump to tokio 1.54.1. The bump does nothing once fix(deps): bump tokio to 1.54.1 for RUSTSEC-2026-0386 #849 merges.
  • RUSTSEC-2023-0089 (atomic-polyfill unmaintained). This one comes from this PR. The new phonenumber crate pulls it in through postcard and heapless 0.7, but only for targets without native atomics, so it is never built for x86_64 or aarch64. I added it to deny.toml's ignore list, with that reason and a note on when to drop it.

cargo deny check passes locally in 484c5fc.


Generated by Claude Code

PoiMatcher now also matches when the ATP feature and the OSM candidate
share a descriptive phone number or email address, and their main
categories are equal or a known pair (src/matchers/contact/
category_pairs.tsv). A brand:wikidata conflict still vetoes the match.
Beyond 150 m, a contact match also needs the street address to agree.

Restaurants, hotels, schools and fuel stations now get a matcher too,
and more categories (cafes, fast food, bars, schools, clinics, doctors,
fitness centres, museums, healthcare=*) get a match mask.

The phone and email parsers move from pipeline::atp::nondescript to
matchers::contact, so both sides are parsed alike. OSM numbers in
national format go through the phonenumber crate, in the ATP feature's
country. See docs/notes/2026-10-25-phone-and-email-matching.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P43kACSyJVwszya8Z3d4qQ
Ignore RUSTSEC-2023-0089 (atomic-polyfill is unmaintained), which
phonenumber pulls in through postcard and heapless 0.7, only for
targets without native atomics.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P43kACSyJVwszya8Z3d4qQ
parse_osm_phone now calls phones::parse_phone_number directly instead
of taking the first item of parse_phone_numbers. With build-mode none,
CodeQL resolved that `.next()` to unrelated iterators in tables::graph
and reported a cleartext-logging flow that does not exist.

Also fill in the planet match count of this change in the note.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P43kACSyJVwszya8Z3d4qQ
@brawer
brawer force-pushed the contact-matching-main branch from d46bc0c to 0b04ded Compare October 25, 2026 13:17
@brawer
brawer merged commit ca68536 into main Oct 25, 2026
9 checks passed
@brawer
brawer deleted the contact-matching-main branch October 25, 2026 13:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants