How names are matched

This article explains the stages taxify() runs to bring an input name to a backbone record: cleaning, exact matching, fuzzy matching, the choice among several records with the same name, and the fallbacks for names no backbone holds. It shows what each stage writes into the result and which arguments change it. Installation is covered in Get started. Most examples match against World Flora Online (WFO), so that every row comes from the same backbone.

library(taxify)

The stages

Each name passes through these stages in order and leaves at the first one that resolves it. match_type records which one did:

  • exact: the cleaned name equals a backbone name.

  • exact_ci: the names are equal after capitals and spelling variants of Latin are made equal.

  • abbrev: an abbreviated genus and the epithet identify one species.

  • fuzzy: the closest backbone name within the fuzzy threshold.

  • rank_fallback: an infraspecific name no backbone holds, resolved to its species.

  • basionym: an unplaced record resolved through the name it was based on.

  • none and out_of_scope: no match, described in Names no backbone matches.

Names of hybrids and aggregates go through two further stages, described in Hybrids and aggregates.

Cleaning

Names in data carry authors, qualifiers, extra spaces and capitals that backbone names do not. Cleaning removes them before any comparison:

raw <- c(
  "Quercus robur L.", "Quercus robur (L.) Sm.", "  Quercus  robur  ",
  "cf. Betula pendula", "Betula pendula s.str.", "QUERCUS ROBUR",
  "Q. petraea"
)
res <- taxify(raw, backbone = "wfo", verbose = FALSE)
res[, c("input_name", "matched_name", "match_type", "qualifier")]
#>               input_name    matched_name match_type qualifier
#> 1       Quercus robur L.   Quercus robur      exact      <NA>
#> 2 Quercus robur (L.) Sm.   Quercus robur      exact      <NA>
#> 3       Quercus  robur     Quercus robur      exact      <NA>
#> 4     cf. Betula pendula  Betula pendula      exact       cf.
#> 5  Betula pendula s.str.  Betula pendula      exact    s.str.
#> 6          QUERCUS ROBUR   Quercus robur   exact_ci      <NA>
#> 7             Q. petraea Quercus petraea     abbrev      <NA>
#> Sources: WFO 2026.06 | cite() for full citations

The first three names match exactly once the author and the spaces are gone. The qualifiers cf. and s.str. are removed from the name and kept in qualifier. QUERCUS ROBUR matches with capitals ignored, as exact_ci. Q. petraea has an abbreviated genus; Quercus is the only genus starting with Q that has the epithet petraea, so the row has match_type = "abbrev".

The exact_ci stage also makes spelling variants of Latin equal: ae and oe with i, y with i, ph with f, rh with r, th with t, and a final ii with i. An epithet written sylvestris in one source and silvestris in another therefore matches without fuzzy matching.

An infraspecific rank stays part of the name. ssp. and Var. are written as subsp. and var., and the cleaned name keeps the rank and its epithet.

Fuzzy matching

A name with no exact match is compared with the backbone names of the same genus, and the closest one within fuzzy_threshold is taken; a name whose genus is misspelt is compared with the names that share its first two letters, as described in Misspelt genera. fuzzy_dist is the number of edits between the two names divided by the length of the longer one. The list below has typing errors of several kinds:

messy <- c(
  "Qurecus robur", "Taraxacum officianle", "Plantago lanceoalata",
  "Trifolium repnes", "Lolium perrene", "Achillea millefolum",
  "Betual pendula", "Ranunculus acris"
)
res <- taxify(messy, backbone = "wfo", verbose = FALSE)
res[, c("input_name", "matched_name", "match_type", "fuzzy_dist")]
#>             input_name         matched_name match_type fuzzy_dist
#> 1        Qurecus robur        Quercus robur      fuzzy 0.07692308
#> 2 Taraxacum officianle Taraxacum officinale      fuzzy 0.05000000
#> 3 Plantago lanceoalata  Plantago lanceolata      fuzzy 0.05000000
#> 4     Trifolium repnes     Trifolium repens      fuzzy 0.06250000
#> 5       Lolium perrene       Lolium perenne      fuzzy 0.14285714
#> 6  Achillea millefolum Achillea millefolium      fuzzy 0.05000000
#> 7       Betual pendula       Betula pendula      fuzzy 0.07142857
#> 8     Ranunculus acris     Ranunculus acris      exact         NA
#> Sources: WFO 2026.06 | cite() for full citations

Taraxacum officianle has two letters swapped, one edit on 20 characters, so its distance is 0.05. Lolium perrene needs two edits to become Lolium perenne, 2 / 14 = 0.143. A name matched by an exact stage has no distance, so fuzzy_dist is NA.

A fuzzy match runs only for names that every exact stage missed, so it never replaces an exact match.

Distance methods

fuzzy_method chooses how edits are counted:

  • "dl" (Damerau-Levenshtein, the default) counts an insertion, a deletion, a substitution and a swap of two neighbouring letters as one edit each.

  • "levenshtein" counts a swap as two edits, a deletion and an insertion.

  • "jw" (Jaro-Winkler) gives a similarity between 0 and 1 that weights agreement at the start of the name; the distance is one minus that similarity.

The same names under each method:

dist_of <- function(method) {
  r <- taxify(messy, backbone = "wfo", fuzzy_method = method,
              verbose = FALSE)
  round(r$fuzzy_dist, 3)
}
data.frame(
  input = messy,
  dl = dist_of("dl"),
  levenshtein = dist_of("levenshtein"),
  jw = dist_of("jw")
)
#>                  input    dl levenshtein    jw
#> 1        Qurecus robur 0.077       0.154 0.021
#> 2 Taraxacum officianle 0.050       0.100 0.010
#> 3 Plantago lanceoalata 0.050       0.050 0.031
#> 4     Trifolium repnes 0.062       0.125 0.013
#> 5       Lolium perrene 0.143       0.143 0.029
#> 6  Achillea millefolum 0.050       0.050 0.010
#> 7       Betual pendula 0.071       0.143 0.014
#> 8     Ranunculus acris    NA          NA    NA

The Levenshtein distance is twice the Damerau-Levenshtein distance for the names with swapped letters (Qurecus, officianle, repnes, Betual) and the same for the others. The Jaro-Winkler distances are smaller throughout, because the shared genus at the start of each name raises the similarity, so a threshold set for "dl" is much looser under "jw".

The threshold

fuzzy_threshold = 0.2, the default, accepts a distance up to 0.2: one edit on a 5-letter name, two on a 12-letter name, four on a 20-letter name. A lower value is stricter:

poa <- c("Poa anua", "Poa pratenss", "Poa trialis")
strict <- taxify(poa, backbone = "wfo", fuzzy_threshold = 0.15,
                 verbose = FALSE)
strict[, c("input_name", "matched_name", "match_type", "fuzzy_dist")]
#>     input_name  matched_name match_type fuzzy_dist
#> 1     Poa anua     Poa annua      fuzzy 0.11111111
#> 2 Poa pratenss Poa pratensis      fuzzy 0.07692308
#> 3  Poa trialis          <NA>       none         NA
#> Sources: WFO 2026.06 | cite() for full citations

Poa trialis needs two edits on 13 characters to become Poa trivialis, a distance of 0.154, which the default threshold accepts and 0.15 rejects.

A whole number caps the edits themselves, whatever the length of the name. fuzzy_threshold = 1 accepts one edit, which fits data whose errors are known to be single letters, such as an optical character recognition (OCR) process that drops or doubles a character:

one_edit <- taxify(poa, backbone = "wfo", fuzzy_threshold = 1,
                   verbose = FALSE)
one_edit[, c("input_name", "matched_name", "match_type")]
#>     input_name  matched_name match_type
#> 1     Poa anua     Poa annua      fuzzy
#> 2 Poa pratenss Poa pratensis      fuzzy
#> 3  Poa trialis          <NA>       none
#> Sources: WFO 2026.06 | cite() for full citations

Jaro-Winkler counts no edits, so it takes only a fractional threshold.

Misspelt genera

The comparison is made within the genus, which keeps it fast on backbones with millions of names and keeps a misspelt epithet from matching a species of another genus. A misspelt genus has no names to be compared with, so for WFO, the Catalogue of Life (COL), COL’s Extended Release and the GBIF Backbone Taxonomy a second pass compares the remaining names with all names that start with the same two letters. Betual pendula above was matched in that pass. A typing error in the first two letters is not found:

taxify("Qeurcus robru", backbone = "wfo", verbose = FALSE)[
  , c("input_name", "matched_name", "match_type")]
#>      input_name matched_name match_type
#> 1 Qeurcus robru         <NA>       none
#> Sources: WFO | cite() for full citations

Checking fuzzy matches

A fuzzy match finds the closest name in spelling, which is not always the intended taxon. Two cases from the backbones:

check <- c("Quercus fantasia", "Quercus robur var. robur")
res <- taxify(check, backbone = "wfo", verbose = FALSE)
res[, c("input_name", "matched_name", "accepted_name", "fuzzy_dist")]
#>                 input_name               matched_name  accepted_name fuzzy_dist
#> 1         Quercus fantasia            Quercus matasii Quercus glauca  0.1875000
#> 2 Quercus robur var. robur Quercus robur var. robusta  Quercus robur  0.1153846
#> Sources: WFO 2026.06 | cite() for full citations

Quercus fantasia is an invented name within the threshold of Quercus matasii. Quercus robur var. robur is not in WFO, and the fuzzy stage reaches var. robusta, three edits away, before the fallback to the species can run. inspect() labels them as likely misspellings, as shown in Checking a name list.

fuzzy = FALSE turns the stage off, for a list that has already been checked:

exact_only <- taxify(messy, backbone = "wfo", fuzzy = FALSE,
                     verbose = FALSE)
table(exact_only$match_type)
#> 
#> exact  none 
#>     1     7

Names with more than one accepted taxon

Some spellings belong to more than one taxon. When a spelling leads to several records, one is picked: the record with the input’s author when the input carries one, otherwise an accepted record before an unplaced one before a synonym, then the species rank, then the lowest ID. Pinus abies was published by Linnaeus for the Norway spruce and again by Thunberg for a Japanese spruce:

pa <- c("Pinus abies", "Pinus abies L.", "Pinus abies Thunb.")
res <- taxify(pa, backbone = "col", verbose = FALSE)
#> Warning: 3 of 3 names resolve to more than one accepted ID (Pinus abies, Pinus
#> abies L., Pinus abies Thunb.). `accepted_id` holds one and `accepted_ids` all
#> of them; list them with taxify_ids() before requesting data by ID.
res[, c("input_name", "accepted_name", "n_ids", "accepted_ids")]
#>           input_name accepted_name n_ids           accepted_ids
#> 1        Pinus abies   Picea abies     4 4HPZF|8K9Y|6BRMR|4HQ4F
#> 2     Pinus abies L.   Picea abies     4 4HPZF|8K9Y|6BRMR|4HQ4F
#> 3 Pinus abies Thunb.  Picea polita     4 4HQ4F|4HPZF|8K9Y|6BRMR
#> Sources: COL 2026.09 | cite() for full citations

n_ids counts the accepted taxa the spelling leads to, and accepted_ids lists their IDs, the pick first. The warning names every row with more than one. The author picks the record in the third row: Pinus abies Thunb. resolves to Picea polita. taxify_ids() lists every candidate of a row with its name, author and status, as shown in Requesting GBIF data for a species list.

A genus name can also be used in two kingdoms. Prunella is a bird genus (the accentors) and a plant genus (self-heal), and COL accepts both. Without further information the pick follows the lowest ID, here the bird. kingdom keeps only the records in the named kingdom:

cols <- c("input_name", "accepted_name", "family", "n_ids", "pick_basis")
default <- taxify("Prunella", backbone = "col", verbose = FALSE)
plant <- taxify("Prunella", backbone = "col", kingdom = "plants",
                verbose = FALSE)
rbind(default[, cols], plant[, cols])
#>   input_name accepted_name      family n_ids pick_basis
#> 1   Prunella      Prunella Prunellidae     2         id
#> 2   Prunella      Prunella   Lamiaceae     2    kingdom
#> Sources: COL 2026.09 | cite() for full citations

With kingdom = "plants" the row moves to the plant genus in Lamiaceae, and pick_basis records that the kingdom decided it. A name with no record in the named kingdom is unmatched, and in a fallback chain it goes on to the next backbone.

Synonyms and basionyms

A name list often contains names a backbone treats as synonyms, and such a row carries the accepted name its record points to, with is_synonym = TRUE. When a name has both a synonym record and an unplaced record, the synonym is picked if it shares its type with its accepted name: the accepted name keeps the synonym’s final epithet and names its author in parentheses.

syn <- taxify(c("Lycopsis orientalis", "Pinus sylvestris"), backbone = "wfo",
              verbose = FALSE)
syn[, c("input_name", "accepted_name", "is_synonym", "taxonomic_status")]
#>            input_name                      accepted_name is_synonym
#> 1 Lycopsis orientalis Anchusa arvensis subsp. orientalis       TRUE
#> 2    Pinus sylvestris                   Pinus sylvestris      FALSE
#>   taxonomic_status
#> 1          SYNONYM
#> 2         ACCEPTED
#> Sources: WFO 2026.06 | cite() for full citations

Lycopsis orientalis L. is such a synonym of Anchusa arvensis subsp. orientalis (L.) Nordh., and it is picked over the unplaced Lycopsis orientalis of another author.

The last stage handles a name that matched only an unplaced record. When that record names the name it was based on (its basionym), and the basionym is placed in the backbone, the row takes the basionym’s accepted taxon, with match_type = "basionym". The other columns still describe the record that was matched.

Infraspecific names

A subspecies or variety that a backbone holds matches as written. One that no backbone holds resolves to its species in the rank_fallback stage:

infra <- c("Quercus robur ssp. robur", "Quercus robur subsp. imaginaria")
res <- taxify(infra, backbone = "wfo", verbose = FALSE)
res[, c("input_name", "matched_name", "match_type", "rank")]
#>                        input_name               matched_name    match_type
#> 1        Quercus robur ssp. robur Quercus robur subsp. robur         exact
#> 2 Quercus robur subsp. imaginaria              Quercus robur rank_fallback
#>         rank
#> 1 subspecies
#> 2    species
#> Sources: WFO 2026.06 | cite() for full citations

ssp. is read as subsp., so the first name matches exactly. rank is the rank of the matched record, so the fallback row shows species although the input named a subspecies.

Names no backbone matches

A name that reaches no record has match_type = "none" or "out_of_scope". The genus register decides between the two: an index of the genera in all 19 backbones, each with its kingdom, family and life form, built over a fixed set of backbones and downloaded like a backbone, so every machine reports the same labels.

un <- taxify(c("Blorbus maximus", "Carcinus maenas"), backbone = "wfo",
             verbose = FALSE)
un[, c("input_name", "match_type", "life_form")]
#>        input_name   match_type life_form
#> 1 Blorbus maximus         none      <NA>
#> 2 Carcinus maenas out_of_scope    animal
#> Sources: WFO | cite() for full citations

Blorbus is in no backbone, so the row is none: the name may be misspelt or invented. Carcinus is a crab genus held by other backbones but not by WFO, so the row is out_of_scope and is not compared by fuzzy matching. The register also fills kingdom_group, taxon_group and life_form on every matched row, whichever backbone matched it. Choosing and combining backbones shows how to find a backbone that holds a genus.

Where next