offline taxonomic name resolution
Hand taxify a column of messy species names. It cleans
them, matches them against a Darwin Core backbone on your disk, resolves
synonyms to accepted names, and returns one standardized data.frame.
Every step runs locally against a versioned snapshot, so there are no
API calls, no rate limits, and the same input gives the same output on
any machine. A list of thousands resolves in seconds, with the matching
engine written in C through the vectra columnar engine.
install.packages("taxify")Or the development version from GitHub:
install.packages("pak")
pak::pak("gcol33/taxify") # vectra is installed automaticallylibrary(taxify)
# the first call installs the default backbone set (COL + GBIF + ITIS, ~4 GB)
taxify(c(
"Quercus robur",
"Pinus abies", # synonym, resolved to Picea abies
"Quercus robus", # typo, fuzzy-corrected to Q. robur
"Taraxacum officinale"
))You get one row per input name on a fixed schema: the matched and
accepted names with their IDs and authorship, rank, family, genus,
epithet, synonym / hybrid / ambiguity flags, the match type, the fuzzy
distance, a coarse kingdom and taxon-group label, and the backbone and
version used. summary() prints how the batch resolved.
result <- taxify(c("Quercus robur", "Pinus abies", "Quercus robus", "Taraxacum officinale"))
summary(result)
#> -- taxify results ----------------------------------------------------
#> backbone: COL | 4 names submitted
#>
#> matched 4 (exact: 2, case-insensitive: 0, fuzzy: 2, abbrev: 0)
#> --------------------------------------------------------------
#> taxon groups: vascular plant: 4taxify ships
19
backbones as compressed .vtr files, pre-built by the
companion taxifydb
package and downloaded once. Pass several and they form a fallback
chain, where a name unmatched by the first cascades to the next. The
chain is staged by match quality: every backbone is asked for an exact
match before any backbone is asked for a fuzzy one, so a near neighbour
in an early backbone does not settle a name a later backbone holds
exactly.
# COL first (all kingdoms), then GBIF for whatever COL leaves open
taxify(c("Quercus robur", "Panthera leo", "Amanita muscaria"), backbone = c("col", "gbif"))Pass no backbone and every installed backbone forms one
chain in a fixed priority order: the COL syntheses (COL Extended
Release, then COL), then the domain authorities (marine, plants, fungi,
algae, fishes, reptiles, mammals, birds, prokaryotes), then the broad
aggregators GBIF, ITIS, NCBI, and OTT.
| Backbone | Scope | Names | Download |
|---|---|---|---|
| WFO | Vascular plants | 1.6M | 761 MB |
| COL | All kingdoms | 5.3M | 2.0 GB |
| COL Extended Release | All kingdoms | 7.9M | 1.6 GB |
| GBIF | All kingdoms | 6.4M | 1.6 GB |
| ITIS | US focus, freshwater/marine | 993k | 205 MB |
| NCBI | All life | 2.8M | 531 MB |
| OTT | All life (synthetic) | 3.7M | 763 MB |
| WoRMS | Marine/aquatic | 1.6M | 312 MB |
| Euro+Med | European/Mediterranean plants | 147k | 35 MB |
| Species Fungorum | Fungi | 315k | 71 MB |
| AlgaeBase | Algae | 172k | 36 MB |
| FishBase | Fishes | 103k | 19 MB |
| SeaLifeBase | Non-fish marine/aquatic | 134k | 29 MB |
| Reptile Database | Reptiles | 50k | 10 MB |
| LCVP | Vascular plants | 1.3M | 252 MB |
| WCVP | Vascular plants | 1.4M | 309 MB |
| Mammal Diversity Database | Mammals | 62k | 11 MB |
| AviList | Birds | 41k | 8 MB |
| LPSN | Prokaryotes (Bacteria/Archaea) | 45k | 12 MB |
list_backbones() returns this table live, with the
installed and version status of each. taxify_databases()
adds the enrichment layers alongside it.
Input names are normalized first, so the fuzzy pass runs only on names that genuinely differ from the backbone:
"Quercus robur L." -> "Quercus robur" # authorship stripped
"Pinus cf. sylvestris" -> "Pinus sylvestris" # qualifier removed
"Nothofagus x alpina" -> "Nothofagus × alpina" # hybrid sign normalized (x -> ×)
"Betula pendula (Roth) Doll" -> "Betula pendula" # parenthesized author strippedFuzzy matching takes Damerau-Levenshtein, Levenshtein, or Jaro-Winkler with a distance threshold, and runs genus-blocked, so a typo competes against names in its own genus.
taxify and WorldFlora both read the same WFO snapshot, which isolates the two matching implementations on identical data. The corpus is 1,000 accepted binomials drawn from the backbone with a fixed seed; the fuzzy corpus is those names with one substituted character in each epithet, so every one has to resolve by distance.
| taxify | WorldFlora | |
|---|---|---|
| Backbone load | 4.9 s | 20.1 s (CSV into RAM) |
| Exact match, 1,000 names | 2.2 s | 17.1 s |
| Fuzzy match, 1,000 names | 18.8 s | 4,192 s (70 min) |
| Fuzzy match, 5,000 names | 26.6 s | not measured |
| Peak R heap, fuzzy 1,000 | 678 MB | 4.0 GB |
scripts/benchmark-worldflora.R produces these numbers
and scripts/benchmark-worldflora-results.json records the
run, including package versions and the backbone snapshot. Both packages
were measured back to back on one machine (Windows 11, R 4.6.0, taxify
0.3.21, WorldFlora 1.14.5) that was carrying other work at the time, so
the ratios are the reliable figures.
taxify() resolves a name to its accepted name. The same
local backbone file answers the related lookups, with nothing else to
download:
synonyms("Picea abies") # every synonym of an accepted name
children("Quercus") # accepted species in a genus
downstream("Fagaceae", downto = "genus") # all genera under a family
upstream("Quercus robur", to = "family") # the family a species sits in
class2tree(species) # a lineage as a Newick / ape phylo tree
lowest_common(species) # the deepest shared rank (the MRCA)
parse_name("Quercus robur (L.) H.Karst.") # genus / epithet / author, no lookup
id2name("2878688", backbone = "gbif") # GBIF usage key -> name + classification
comm2sci("pedunculate oak") # common name -> scientific
sci2comm("Quercus robur") # scientific -> common names
reconcile(old_species_list) # how a checklist maps onto the backbone
taxify_lock(result) # freeze the backbone + enrichment versions
cite(result) # citations for every source used108 enrichment layers join published trait and status data to a result through the backbone-resolved accepted name, so synonyms in either dataset land on the same key.
taxify(plant_names) |>
add_iucn() |> # IUCN Red List
add_griis("AT") |> # GRIIS invasive status
add_zanne() |> # Zanne et al. woodiness
add_eive() # EIVE indicator values
taxify(fish_names) |>
add_fishbase() |> # FishBase morphology and ecology
add_fishmorph() # FISHMORPH functional traits
taxify(plant_names) |>
add_trait("seed_mass") # every source that carries it, harmonized to mgSources span all kingdoms: IUCN, GRIIS, GBIF common names, WCVP,
EIVE, Diaz et al., LEDA, GIFT, FungalTraits, FUNGuild, AlgaeTraits,
EltonTraits, AVONET, PanTHERIA, AmphiBIO, FISHMORPH, FishBase, AnAge,
GloNAF, LepTraits, AnimalTraits, and regional plant-trait sets for
France (Baseflor), Britain (Ecoflora), and Germany (FloraWeb), among
others. list_enrichments() returns the full set in R,
list_traits() browses the cross-source trait vocabulary
behind add_trait(), and the enrichments
vignette lists every source with its reference and license.
add_data() joins your own table the same way,
auto-detecting the species column and matching it through the backbones
used in the original call. It reads data.frames, CSV, CSV.GZ, XLSX,
SQLite, and .vtr.
result |> add_data("TRY_traits.csv")
result |> add_data("TRY_traits.csv", cols = c("LeafArea", "SLA", "PlantHeight"))inspect() returns only the names that look wrong, each
labelled with what stands out and the name to use instead: typos,
retired synonyms, made-up genera, near-duplicate spellings, and the lone
animal in a list of plants. Each label is ranked by whether it needs a
decision, a second look, or optional cleanup.
inspect(field_names) # offline register and list checks
inspect(field_names, backbones = TRUE) # also typos, synonyms, ambiguityFor a regional field list, region steers fuzzy
correction toward species that occur where you work, so a misspelling
resolves to the plant that grows there. Pass a region name, a TDWG code,
or coordinates.
taxify(field_names, region = "Belgium")
taxify(field_names, coords = c(4.35, 50.85))Bug reports and questions go to the issue tracker.
“Software is like sex: it’s better when it’s free.” — Linus Torvalds
I’m a PhD student who builds R packages in my free time because I believe good tools should be free and open. I started these projects for my own work and figured others might find them useful too.
If this package saved you some time, buying me a coffee is a nice way to say thanks. It helps with my coffee addiction.
MIT (see the LICENSE file)
@software{taxify,
author = {Colling, Gilles},
title = {taxify: Offline Taxonomic Name Matching Against Darwin Core Backbones},
year = {2026},
url = {https://github.com/gcol33/taxify}
}Cite the backbones and enrichment layers you actually used with
cite(result), which pulls each source’s own reference from
the manifest.