Skip to content

fuzzy_match(): require an aff/cf/x qualifier token on both sides - #295

Merged
ehwenk merged 1 commit into
masterfrom
fix/291-294-affinis-hybrid-fuzzy-match
Aug 24, 2026
Merged

ehwenk merged 1 commit into
masterfrom
fix/291-294-affinis-hybrid-fuzzy-match

Conversation

@ehwenk

@ehwenk ehwenk commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #291 and #294: "Acacia aff. aneura" ("resembles Acacia aneura, not
confidently identified") was resolving all the way to the unrelated accepted
species "Acacia aptaneura" instead of being capped at genus rank.

Root cause

fuzzy_match()'s check_match() confirmation step only compares as many
word positions as the shorter of query/candidate has. For
"acacia aff aneura" (3 words) against "acacia aptaneura" (2 words), it
only checks word 1 (acacia==acacia) and word 2 — and word 2 of the query
is the qualifier "aff", word 2 of the candidate is the whole epithet
"aptaneura", both coincidentally starting with "a". The real epithet
"aneura" (word 3) is never compared to anything.

Combined with match_05 (species-level fuzzy match) running before
match_06 (aff./cf. detection) and match_08 (hybrid detection), this
let affinis- and hybrid-qualified names slip past the dedicated handling
entirely.

Why not just reorder the match steps (the initially-proposed fix on
#291)? resources$APC_synonyms has 1,156 real names containing literal
aff./cf. (e.g. "Isoetes sp. aff. muelleri") that legitimately need
match_05b to run first so a typo'd version still resolves to its real
name, rather than being caught by the qualifier heuristic and capped at
genus rank. The same reasoning applies to match_08 and real named hybrids
(e.g. "Persoonia x lucida", "Eucalyptus x alpina" — both real accepted
species). Full trace is in the issue threads.

Fix

In check_match(): if the query contains "aff", "cf", or "x" as a
standalone word, require the candidate to contain that same token too, or
reject the candidate outright before the coarser first-letter word-position
check runs. This is existence-based, not position-based, so ordinary
word-count drift elsewhere in phrase names (locality/collector text) is
untouched — it only activates when a qualifier token is actually present in
the query.

devtools::load_all()
resources <- load_taxonomic_resources(stable_or_current_data = "stable", version = "2024-10-11")

# before: "acacia aptaneura"  |  after: NA
fuzzy_match(txt = "acacia aff aneura", accepted_list = resources$APC_accepted$stripped_canonical,
            max_distance_abs = 3, max_distance_rel = 0.2, n_allowed = 1)

# still works: real aff.-qualified synonym, typo'd
fuzzy_match(txt = "isoetes sp aff muellerii", accepted_list = resources$APC_synonyms$stripped_canonical,
            max_distance_abs = 3, max_distance_rel = 0.2, n_allowed = 1)
#> "isoetes sp aff muelleri"

# still works: real named hybrid, typo'd
fuzzy_match(txt = "persoonia x luciida", accepted_list = resources$APC_accepted$stripped_canonical,
            max_distance_abs = 3, max_distance_rel = 0.2, n_allowed = 1)
#> "persoonia x lucida"

Tests

New tests/testthat/test-functions-fuzzy_match.R:

  • fuzzy_match() unit tests for both the fix (aff/x no longer confirm
    against an unrelated candidate) and the preserved legitimate cases above.
  • An align_taxa() end-to-end test confirming "Acacia aff. aneura" now
    resolves via match_06a at genus rank, not a wrong species match.

Verification

NOT_CRAN=true devtools::test() — 188 passed, 0 failed (the only other
failures are 3 pre-existing, network-only tests in test-connection.R that
require live internet access unavailable in this sandbox; unrelated to this
change). Includes the full match_branches snapshot (the safety net for
match_taxa()) — no regressions.

Fixes #291. Fixes #294.

🤖 Generated with Claude Code

… sides

check_match()'s word-position confirmation check only ever compares as
many word positions as the shorter of query/candidate has. That let a
qualifier word in the query -- "aff"/"cf" (uncertain identification) or
"x" (hybrid marker) -- get silently absorbed into an unrelated word's
position purely by first-letter coincidence:

  "acacia aff aneura" (3 words) confirmed against the unrelated accepted
  species "acacia aptaneura" (2 words), because word 2 of the query
  ("aff") and word 2 of the candidate ("aptaneura") both start with "a"
  -- the real epithet "aneura" (word 3) was never compared to anything.

match_05 (species-level fuzzy match) runs before match_06 (aff./cf.
detection) and match_08 (hybrid detection), so this let affinis- and
hybrid-qualified names resolve all the way to an unrelated species
instead of being correctly capped at genus rank. Reordering those match
steps earlier is not a safe fix on its own: APC_synonyms has 1156 real
names containing "aff."/"cf." that legitimately need match_05b to run
first (see #291), and the same reasoning holds for match_08 and real
named hybrids.

Fix instead in fuzzy_match()'s check_match(): if the query contains
"aff", "cf", or "x" as a standalone word, require the candidate to
contain that same token too, or reject outright before the coarser
first-letter word-position check runs. This is existence-based rather
than position-based, so it tolerates ordinary word-count drift elsewhere
in phrase names (locality/collector text) -- only activates when a
qualifier token is actually present in the query, and a typo'd version
of a real aff./cf./hybrid-named entry still matches, since the token is
present on both sides (verified against "Isoetes sp. aff. muelleri" and
"Persoonia x lucida", both real APC entries).

Verified against the full match-step benchmark + match_branches
snapshot: 184 passed, 0 failed, no regressions.

Fixes #291, #294.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@ehwenk
ehwenk requested a review from dfalster August 24, 2026 00:20

@dfalster dfalster left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks reasonable, but I couldn't tell if it actually works without testing against data which you've done

@ehwenk
ehwenk merged commit 4de56fc into master Aug 24, 2026
4 checks passed
@ehwenk
ehwenk deleted the fix/291-294-affinis-hybrid-fuzzy-match branch August 24, 2026 02:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants