taxify (development version)
Documentation
- The README and every article were rewritten. Get started is now
vignette("taxify")(the oldquickstartURL redirects), and the articles are grouped into workflows and background. New articles cover exploring the backbone (synonyms(),children(),upstream(), …) and matching vegetation units. The per-source detail of every published dataset moved to The backbones and datasets. - The output shown in the README and the articles comes from running their code against the installed backbones (
data-raw/precompute_vignettes.Rknitsvignettes/*.Rmd.orig), so it no longer drifts from what the functions return. -
citation("taxify")now readsinst/CITATION, with the CRAN DOI, andcite()takes the package’s own entry from it.
Bug fixes
-
add_data(), the cross-backbone recovery of theadd_<source>()functions andinspect(backbones = TRUE)no longer repeat thetaxify_multiple_idswarning for names they match internally (the joined table’s names, a dataset’s names); the warning still names the inputs of thetaxify()call itself. -
taxify(kingdom = )now moves a name used in two kingdoms to its record in the requested kingdom instead of leaving it unmatched:Prunellawithkingdom = "plants"matched the bird genus, dropped it, and returned nothing, although COL also accepts the plant genus. The row’spick_basisis"kingdom". -
cite()and the “Sources:” line under a printed result name only the backbones that matched a row, not every backbone of the fallback chain, and the line no longer printsNAfor a backbone without a version. -
add_col_info(),add_wfo_info(),add_gbif_info()andadd_hybrid_info()no longer register themselves as published datasets, which madecite()list an empty entry (“COL (2025). col_info.”) andtaxify_lock()record a datasettaxify_restore()could never find. Their columns come from a backbone the result already cites. -
add_data()records the joined table undertaxify_meta$user_datainstead of among the published datasets, socite()no longer lists it andtaxify_restore()no longer reports it as missing. - Exact matching of long lists is several times faster: choosing the pick of each name scanned the whole candidate table once per name, so 100,000 names took about four times as long as needed.
-
inspect(backbones = TRUE)matches in the order of the default fallback chain, so its labels are the onestaxify()gives. - A subset of an
inspect()report is a plain data frame; it printed as an empty report. -
gbif_request(dry_run = TRUE)prints the keys alone instead of the key table and the match metadata attached to them. -
list_backbones()gains atitlecolumn with each backbone’s full name. - The benchmark scripts
scripts/benchmark-memory.Randscripts/benchmark-worldflora.Rcalledtaxify()with an argument it no longer has;scripts/benchmark-scale.Ris new and times lists of 1,000 to 100,000 names.
taxify 0.7.0
Vegetation units (syntaxa)
- Two syntaxon backbones match vegetation-unit names the way the others match taxon names:
evc, the EuroVegChecklist (Mucina et al. 2016, current version from FloraVeg.EU: 150 classes, 381 orders, 1,310 alliances and their synonyms), andvegcz, the national classification of the Czech Republic (Chytrý 2007-2013, from Pladias: classes, alliances and 496 associations). Name one intaxify(backbone = ); the default chain stays with taxon names, and the two kinds are not combined in one call (#96). - A syntaxon result carries the accepted unit’s
class,order,allianceandassociationin place offamily,genusandepithet, so a name at any rank rolls up. A Czech association reaches its EuroVegChecklist alliance throughvegcz(which places it under its alliance) and thenevc. - Names are read under the phytosociological code: the author citation (
(Neuhäusl 1959) Oberd. et al. 1967), annotations (nom. inval. (5a)), a database code in front (TDA - Arrhenatherion elatioris), rank-labelled lists (all. X, ord. Y) and code-page mojibake are parsed off, and the rank is read from the suffix.parse_name(x, code = "syntaxon")returns the parse. - A unit written without its epithets (
Cynosurionfor Cynosurion cristati) resolves with the newmatch_type = "short_name"when its stems name one unit; otherwise it stays unmatched with the candidates inaccepted_ids. Fuzzy matching of a syntaxon keeps its rank and the plant names it is built from, soVaccinio-Piceionis not matched to the different allianceVaccinio-Pinion. - The backbone registry records each backbone’s nomenclature code (
backbone_code(), thecodecolumn oflist_backbones()), and the name cleaner, normalization key and stages a backbone runs are read from that code.
Accepted-ID columns
-
n_idsandaccepted_idsare in everytaxify()result:1and theaccepted_idfor a name with one accepted ID,NAfor an unmatched row. Two results now alwaysrbind(), andx$n_ids > 1isFALSErather thanlogical(0)on a list without homonyms. Printing still leaves the two columns out when no row has more than one ID (#92).
Requesting GBIF data
-
gbif_request(keys = c("taxon", "pick", "all"))replacesstrict.taxify_ids()gives each ID arole:pick,same_name(another record of the same name, such as GBIF’s doubtful Pinus sylvestris homonyms) orother_taxon(the accepted taxon of a synonym record under another author, a different species). The default"taxon"requests the first two and names the keys of other taxa it leaves out; previously every key was requested, so Absinthium vulgare returned two species under one name."pick"is the oldstrict = TRUE,"all"the old default (#93). - A default-chain result follows the taxon each row picked: a crosswalked row takes its pick’s GBIF key, a row without one is re-matched against GBIF by its
accepted_namerather than its input name. With the GBIF backbone installed, GBIF’s further records of the picked name and the occurrence counts of crosswalked keys are read from it, so the same list requests the same keys throughbackbone = "gbif"and through the default chain, and the size estimate covers every key. Thetaxaattribute gainskey_source(backbone/crosswalk/rematch) andrequested(#93).
Picking between homonyms
- Publication year orders same-name homonyms only when every candidate at that level has a year; a dated record beside an undated one no longer wins for being dated. A recombination is dated by its own publication, not its basionym’s (ICN Art. 53.1): Rhus hirta (L.) Sudw. (1892) now loses to Rhus hirta Harv. ex Engl. (1883) (#91).
-
taxify()results andtaxify_ids()carrypick_basis, what decided the pick over a name’s other accepted IDs (status,priority,occurrences,validity,fuzzy,authorship,basionym, oridwhen only the lowest taxon ID separated them, which settles nothing) (#91). -
add_wfo_info()andadd_col_info()return the publication reference asname_published_in, the nameadd_gbif_info()uses; it reads the unified column taxifydb now writes for every backbone (gcol33/taxifydb#60).
taxify 0.6.0
CRAN release: 2026-09-30
Genus register kingdoms
- The genus register (
genus_register.vtr, which fillskingdom_group,taxon_groupandlife_form) is rebuilt: its kingdom is read from accepted genus records before synonyms, a spelling shared by genera in two kingdoms resolves to the genus with more accepted species, a genus’s ranks all come from one of the genera sharing its spelling, and GBIF’s own kingdom column is read. Genera without a kingdom fall from 44,024 to 2,978 of 502,381, and 5,767 change kingdom. Olea europaea readkingdom_group = "animalia"(the register had picked the sea slug Olea), and Aa, Abelia and Acaena read as animals from synonym records; Panthera and Pteropus carried a moth’s and a stick insect’s family. - The register’s new
multi_kingdomcolumn marks the 5,026 genera whose spelling accepted records place in more than one kingdom. For those, a matched row’skingdom_groupis the kingdom of the record it matched, read from the backbone’skingdomcolumn, andtaxon_group/life_formareNAwhere that kingdom differs from the register’s. - A backbone scoped to one kingdom (WFO, Euro+Med, LCVP, WCVP, Species Fungorum, MDD, AviList, ReptileDB, FishBase) always sets a matched row’s
kingdom_groupto that kingdom. -
taxify_register_coverage()reports the release each backbone was indexed from; the previous coverage table had none for 17 of 19 backbones.
Euro+Med distribution status
-
add_euromed_distribution()attaches the status Euro+Med PlantBase states for a taxon in each Euro+Med area:euromed_statusisnative,naturalised,introduced,casual,cultivated,doubtfulorextinct, andeuromed_status_detailkeeps the term as Euro+Med words it (endemic,introduced: uncertain degree of naturalisation). The citing references of each record are ineuromed_status_source(resolve withcite(x$euromed_status_source, source = "euromed_distribution")), andcols = "all"adds the taxon’s Euro+Med UUID astaxon_id, thetaxon_idof theeuromedbackbone.regiontakes ISO 3166-1 alpha-2 country codes or Euro+Med area codes. A report Euro+Med marks as made in error is not carried, and a former presence isextinct(issue #90). -
euromed_areas()lists the areas with the ISO code of those that are exactly one country. A combined area (Italy with San Marino and Vatican City, Ireland with Northern Ireland) has none; its countries are read from the level-2 areas (Au(A)Austria,Au(L)Liechtenstein). - The status is harvested per accepted taxon from the europlusmed.org portal taxon pages, served from the CyberTaxonomy CDM which has no bulk export, by
crawl_euromed_portal.pyin taxifydb.
Requesting GBIF data for a name list
-
gbif_request()takes a species list, matches it against the GBIF backbone and requests occurrence data for every accepted taxon key the matches resolve to. GBIF serves records by taxon key rather than by name, and a name it files under several keys (a homonym, or a name held once as accepted and again as a doubtful record) needs all of them: of the four species in the new vignette, Quercus robur has three keys and Pinus sylvestris six.strict = TRUEnarrows the request to the keytaxify()reported, and reports how many keys and records that leaves behind. It is off by default. -
taxify_ids()gains agbif_keycolumn: the legacy GBIF taxon key(s) of each accepted ID, which are what GBIF serves occurrence records for. On thegbifbackbone it is the ID itself; on a COL XR build that carries a GBIF crosswalk it is the|-delimited set of GBIF keys for the same name, several where GBIF files it under more than one.gbif_request()reads it, so a default-chain result whose names are on COL XR is requested without a second match pass over the GBIF backbone; only names on a backbone with no crosswalk, on a COL XR build that predates it, or that resolve to several accepted taxa (COL XR files a homonym’s synonyms under other species) are still re-matched against GBIF.taxon_idandaccepted_idstay COL XR’s. -
method = "download"(default) submits one asynchronous GBIF download covering every key as a singletaxonKey IN (...)predicate, so the list returns as one dataset under one citable DOI. It needs a GBIF account inGBIF_USER/GBIF_PWD/GBIF_EMAIL, and says which are missing before contacting GBIF.method = "search"sends unauthenticated searches and returns the records directly, with ataxon_key_requestedcolumn tracing each row to the key that asked for it. - Before a request is sent, the expected size is reported from the backbone’s own occurrence counts, and
dry_run = TRUEreturns the keys while contacting nothing. An ID that is not a GBIF integer key is refused rather than sent, which is what happens against the bundled example database. - A result from the default backbone chain works without being re-matched by hand. The chain starts at COL XR, whose alphanumeric IDs GBIF does not serve records for, so
gbif_request()re-matches every row without a GBIF key against the GBIF backbone and keeps the rows that already had one. A mixed result, where the one name only GBIF carries landed ongbif, keeps all of its names rather than dropping the others. - rgbif moves into Suggests. Resolving names and reading off their keys stays offline and does not need it; only the request itself does.
-
gbif_fetch()waits for a submitted download, retrieves it, imports it and attaches the queried names, so the records come back in one call instead of a wait/get/import loop. A split request is fetched the same way: every download is waited for and the frames stacked on the union of their columns, since GBIF returns only the fields a record carries. -
gbif_backmatch()links the returned occurrence records back to the names they were requested for. Joining ontaxonKeyalone loses records without saying so: GBIF returns a key’s descendants too, and a record identified below species level carries its own key, so a request for Pinus nigra returns rows whosetaxonKeyis its subspecies and which carry the requested key only inspeciesKey. On a 300-record sample of that request ataxonKeyjoin matches 250;gbif_backmatch()matches 300. - A list too long for one GBIF query is split automatically. GBIF caps a download query at 12,000 characters, which a taxonKey predicate reaches at about 1,187 keys (10 characters per key plus 125 of envelope, measured against
occ_download_prep()), so a few hundred names can exceed it once their homonyms are counted.gbif_request()chunks at 1,000 keys and submits through rgbif’socc_download_queue(), which respects GBIF’s limit of three concurrent downloads. Each chunk gets its own DOI andcite()reports all of them. -
cite()reports a GBIF download’s DOI beside the backbone citations. The download key travels fromgbif_request()throughgbif_backmatch()onto the records, and the DOI is read from GBIF at citation time, since GBIF issues it only once the download has finished preparing. A download still running is reported as having no DOI yet, and an unreachable GBIF leaves the backbone citations intact rather than failing.file =writes the download as a BibTeX@miscentry too. - New vignette,
vignette("gbif-requests"), which also covers installing rgbif and storing GBIF credentials.
Names with several accepted IDs
-
taxify()output replacesis_ambiguousandambiguous_targetswithn_idsandaccepted_ids: how many distinct accepted taxa the backbone files the matched name under, and all of those IDs (|-joined, the one inaccepted_idfirst). The old flag fired only when candidates tied in the top priority tier, so a name GBIF holds both as an accepted record and as a doubtful or duplicate one came back with one ID andis_ambiguous = FALSE.n_idscounts every record of the name. In a fuzzy match only names at the pick’s distance count: a farther neighbour is a worse reading of the input, not another taxon it names. -
taxify()warns once per call when any name resolves to more than one accepted ID, naming the count and a few examples. The warning has classtaxify_multiple_ids; switch it off withoptions(taxify.warn_multiple_ids = FALSE). The verbs built ontaxify()(reconcile(),synonyms(),upstream(), …) do not repeat it.summary()shows the count as amultiple idsline. -
taxify_ids()lists every accepted ID of each matched name, one row per (name, ID), with accepted name, authorship, rank, status, family, the GBIF occurrence count and which IDtaxify()picked. It replacestaxify_candidates(), which only expanded tied homonyms. -
n_idsandaccepted_idsappear intaxify()output only when some name resolves to more than one accepted ID, which is also when thetaxify_multiple_idswarning fires (unless switched off). Otherwiseaccepted_idsays all there is to say and the two columns are left out.taxify_ids()andreconcile()work either way (reconcile()always carriesn_ids). Results from two calls can then differ in columns: combine them withdplyr::bind_rows(), which fills the missing columns withNA, rather than baserbind(). - On the GBIF backbone the pick now reads occurrence records: among the records that keep a name as their own concept (accepted, doubtful, unplaced), a key with any GBIF occurrence records beats a key with none, before taxonomic status is consulted. A synonym record never wins on records, since its accepted ID is another species and a download by it returns that species’ data; its key stays listed in
accepted_idsandtaxify_ids(). Between keys that both have records, or both have none, status decides as before, and the count only breaks a tie status, rank and epithet leave. Checked against the live GBIF API: Karwinskia mollis went to the doubtful Standl. key (0 records) and now goes to the accepted Schltdl. key (663). - Among records the status, rank and epithet steps leave level, typically one name published by several authors, the earliest-published record wins before the occurrence count is read (priority of publication; the year comes from GBIF’s
yearorname_published_in). Absinthium vulgare Lam. (1779) resolves to Artemisia absinthium rather than to the Dulac homonym (1867). Years order names under the botanical code only (plants, fungi, chromists, protozoa) and only between records of one kingdom: priority does not cross codes, and the zoological code protects prevailing usage over strict priority, which the occurrence count tracks. - A fuzzy match on GBIF now reads occurrence counts and publication years too; the fuzzy path used to drop those columns. Needs the
gbif-2023.08backbone (built 2026-09-22), which also carries GBIF’sDOUBTFULstatus instead of folding it intoACCEPTED. -
reconcile()marks a name"ambiguous"when it has several accepted IDs and does not resolve to itself as accepted, and gains ann_idscolumn; a name accepted under its own spelling stays"unchanged"when a homonym record also exists.inspect()’sambiguouslabel follows the same rule. - The abbreviated-genus stage (
"Q. robur") considers only names with as many words as the query, so a binomial no longer draws the infraspecific names built on it into its candidates.
Session cache
- The session cache of resolved backbone and enrichment paths is scoped by data directory. Pointing
taxify.data_direlsewhere mid-session (at the example database, at a second install) now reads that directory’s files; a backbone opened under the previous directory used to keep answering under the same name.
taxify 0.5.6
Cross-backbone recovery in grouped doors (
add_wcvp()and the others over an authorship-bearing source) now also runs for a name whose own concept the source does not hold under that name, even where parts of the taxon supplied values, and for each part of the matched taxon that found nothing under its own name. A part is asked of the source’s own backbone only when the source is also a backbone. When the matched backbone is known, an alternative is taken only if it lies inside the matched taxon: one at a broader rank (a species offered for a subspecies) is refused, and so is one the matched backbone accepts as a taxon of its own, unless that accepted record is a homonym under another author. Over 10,000 wcvp keys through COL / WFO / GBIF this gains 10,062 / 4,894 / 5,193 cells and drops 4,966 / 11,975 / 8,782. Every dropped cell traces to one of the two refusals (switching the check off restores all of them): a species’ range under an infraspecific name, or an independent taxon’s range (Dianthus carthusianorum’s under D. rogowiczii, which WFO accepts on its own). Each “accepted elsewhere” refusal behind a dropped cell rests on agreeing authors or on an autonym.A grouped door now reports what the within-taxon check left empty: the recovery message counts the names,
summary()shows the count, and the enrichment’staxify_metaentry carriesn_refusedplus arefusedtable (queried name, refused name, reason, the backbones offering it, and how many requested groups it would have filled). Through COL / WFO / GBIF over the same 10,000 names that is 2,284 / 2,001 / 2,664 names.add_wcvp(): where parts of the matched taxon disagree on a region,nativewins overintroducedandintroducedoverextinct, since a taxon is native wherever any of its parts is; ties go to the name’s own record, then the parts by name. Previously the first part read won. This changes 126 / 159 / 197 cells through COL / WFO / GBIF, all tonative.Faster grouped enrichment: the backbone reads behind the concept pick use the cached in-memory block, a
.vtr’s schema is read once per session, author citations are parsed once per distinct string, and the group fill andpick_best_vec()’s ambiguity flag no longer scale with the square of the group count.add_wcvp(region = "all")over 10,000 names through GBIF, including the wider recovery above, went from 53 s to 22 s.add_griis(country = "all")over 10,000 GRIIS keys went from 29.6 s to 3.7 s through WFO and from 10.4 s to 4.4 s through GBIF, with output identical cell for cell: GRIIS carries no authorship column, so neither the wider recovery nor the within-taxon check applies to it.-
add_wcvp()(and every grouped door over an authorship-bearing source) now returns the range of the taxon a name was matched to, as the matching backbone draws it. The build keys a source concept under every name a backbone gives it, so the rows under one name belonged to several concepts and the guard only ever separated two authored ones:- A row with no authorship is an autonym (WCVP writes no author for one). It is the name’s own concept under an autonym key and another name’s autonym anywhere else. Erigeron pulchellus Michx. came back native in Turkey, Iran and the Caucasus from E. caucasicus subsp. caucasicus; 1,042 names in the current
wcvpasset carried another name’s autonym this way (40,447 rows). - A name’s range is its own concept plus the concept of every name the matched backbone places inside it (its synonyms and, for a species, its accepted infraspecific taxa and their synonyms), each read under its own key and picked by the author the backbone gives it. Through COL and GBIF, which hold Eucalyptus bicostata as a subspecies or variety of E. globulus, E. globulus includes New South Wales; through WFO, which keeps E. bicostata a species, it does not. A subspecies filed only under its own name now counts for its species.
- Rows under the name that no part of the taxon accounts for are dropped, also when they are the only rows there. The largest group is a species’ range filed under one of its subspecies or varieties.
- An author citation is matched as sources vary it (initials, the part before
ex,fil.forf., a dropped basionym author, a co-author left out, a one-letter typo), so a name’s own concept is still found when the backbone and the source cite it differently, whileL.andL.f., orRoseandLowe, stay apart.
Over 10,000
wcvpkeys (half at random, half holding several concepts), matched through COL / WFO / GBIF, against the previous release: 8,595 / 8,574 / 39,964 region cells are gone, of which 5,450 / 4,761 / 34,770 were a species’ range under one of its subspecies or varieties and 1,005 / 954 / 710 another name’s autonym; 4,188 / 5,823 / 9,457 cells are new, from the parts of each taxon. Names left with no range: 19 / 5 / 25. - A row with no authorship is an autonym (WCVP writes no author for one). It is the name’s own concept under an autonym key and another name’s autonym anywhere else. Erigeron pulchellus Michx. came back native in Turkey, Iran and the Caucasus from E. caucasicus subsp. caucasicus; 1,042 names in the current
enrichment_authorship_col(), the columnenrich_by_group()’s homonym guard reads, is exported (internal) so taxifydb keys each region row of an authorship-bearing enrichment on the concept the guard will accept (gcol33/taxifydb#58).Every backbone entry in the bundled manifest now records
source_date, the date the source gives the release a build was made from (an ISO date, orYYYY-MM/YYYYwhere the source dates it no finer), solist_backbones(),taxify_databases()and thecite()data-version note report it for all nineteen backbones rather than GBIF alone. taxifydb records it at build time from the same place it reads the version (#88).
taxify 0.5.5
CRAN release: 2026-09-18
taxify_regions()’s two search examples ("belgium","Europe") are wrapped in\donttest{}, afterR CMD check --as-cranflagged the file at 6.4s elapsed against the 5s per-example limit; the coordinate-lookup example ahead of them still runs unwrapped.enrich_by_group()’s homonym-collision warning (used byadd_wcvp()and every other grouped door) now distinguishes two reasons a name’s rows were leftNA(#87). “match more than one concept” stays for a genuine tie – at least one candidate is a plausible near-miss or shares the query’s basionym author, but more than one does. A new “none share any authorship” warning covers the case where no candidate shares anything with the queried authorship at all: not a tie to break, but a sign the matched concept may not be recognised by the enrichment under any spelling (WCVP sinking an infraspecific WFO concept into its species, for instance), which previously read as a near-miss the matcher gave up on.Breaking:
add_alien_first_records()joins on the source’s own locations and takeslocation =in place ofcountry =. FirstRecords records islands and other parts of a country as regions of their own (“282 non-overlapping regions (countries and sub-national regions such as islands)”, Seebens et al. 2017), so its United States of America excludes Hawaii and Alaska and its Spain excludes the Canary and Balearic Islands. The enrichment used to credit each of those to its country and keep the earliest year, which in v4.0 dated 665 of the source’s species x country first records from an island’s earlier year (337 of them from Hawaii, by a median of 25 years) and gave 3,541 country records to species the source records only on an island. A country is still keyed on its ISO 3166-1 alpha-2 code (location = "AT"); a sub-national location is keyed on its name (location = "Hawaii"), listed byenrichment_groups("alien_first_records"). Every row carriescountry_codefor an explicit roll-up to countries.dispersal_syndromereads BROT 2.0 (gcol33/taxifydb#57). BROT writes dispersal mode as vector letters, most important first, and no pattern matched a letter, so BROT supplied nothing; the slot now maps the first letter through the legend of the BROT 2.0 data paper (Tavsanoglu & Pausas 2018). G (“autochory, by Gravity (=unassisted dispersal)”) reads asgravity, the class Baseflor, AusTraits and LEDA fill from barochory.dispersal_syndromeleaves a term that names no mechanism unplaced, so a source that does name one fills the species. Autochory is the self-dispersal umbrella in LEDA and AusTraits, not ballistic; LEDA’s blastochor was read as gravity, herpochor (a diaspore crawling on hygroscopic awns) as ballistic, and bythisochor (non-floating seeds carried along the bottom of running water) as gravity; AusTraits’undefined(“Dispersal mechanism unknown”) was read as unspecialized. All are nowNA; a co-listed vector still wins (autochory, myrmecochoryisant). GIFT’sunspecializedkeeps its class. Each source note carries the definitions and the shared-species calibration.LEDA is read as taxifydb’s corrected reader builds it (gcol33/taxifydb#57).
leda_seed_mass_mgcalibrates at 1.00 against Kew SID, GIFT and BIEN and now feedsseed_mass;ssd_g_cm3is g/cm3 as built, sowood_densityno longer divides it by 1000 and carries a caution (most records are air-dry densities, 1.14x GWDD on 37 shared species).add_leda()attachesclonal_growth_organandfloating_capacity_1week_pctin place of the emptyclonal_growthandbuoyancy.add_bien()andadd_ecoflora()document the unit of every curated numeric column. The rebuilt assets write BIEN leaf dry mass in mg (BIEN records g) and Ecoflora typical maximum and minimum height in mm (Ecoflora records cm).taxify requires vectra (>= 0.12.4), which ties each
.vtriindex to the store it was built for and drops stale indexes when a.vtris rewritten.
taxify 0.5.3
Five fixes in the core matching path, all of them cases where a result depended on something other than the name being matched: how often it was submitted, which backbone happened to answer a neighbouring name, or which build was loaded earlier in the session.
Several queries may now resolve to the same backbone row (#56). Fuzzy matching kept only the closest query per target and returned
match_type = "none"for the rest, to stop two different species collapsing onto one row. But two inputs sharing a target is the normal shape of a dirty checklist: a repeated misspelling, or two spellings of one name. The rejected input came back as if the backbone held nothing near it, and which input was rejected depended on how the batch was ordered. Every query within threshold now matches its best target and carries its ownfuzzy_dist.The loaded backbone and its compact fuzzy copy are keyed on the build, not on the file’s basename (#57). Both were memoized under
.blk_<basename>, so ataxify_restore(install = TRUE), a mid-session update and a data-dir switch – all of which swap builds under one basename – left the rest of the session matching against the replaced build while the metadata reported the new one.clear_backbone_memo()now owns that state and runs wherever a backbone path is invalidated, so the loaded copy cannot outlive the path that resolved it.The authorship tiebreak writes
accepted_authorship(#58). Resolving a homonym by the author the query carried rewroteaccepted_namebut left the author of the candidate it had just rejected, so name and author named two different taxa.Abbreviated-genus resolution reads the genus context off the whole query (#59).
"Q. petraea"is disambiguated by a genus written out in full elsewhere in the batch, but each backbone in a fallback chain sees only the names still unmatched – so a genus an earlier backbone had already answered was invisible to the later ones, andmode = "fallback"disagreed withmode = "agreement"on the same query.Three smaller inconsistencies in the core path (#60).
clean_one(), an unreachable and drifted copy ofclean_names(), is deleted andnames_dfis now required, soclean_names()is the single cleaner. AnNAinput getsmatch_type = "none"like"", which is what the documented vocabulary says (match_typeis neverNA). Andsummary()prints each backbone’s own version next to it, rather than labelling a whole chain with the first matched row’s build.
Five fixes in the side paths around matching: joining custom data, pinning and locking a build, the less common enrichment joins, and empty input.
add_data()joins on the accepted taxon, not a bareaccepted_id(#61). Backend ids are integers in most backbones and carry no namespace, so a name matched by ITIS could join onto an unrelated taxon GBIF happened to number the same, with no warning. The id is now qualified by the backbone that issued it, and rows matched by different backbones on the two sides join throughaccepted_name, which is what the synonym promise in the documentation needs.A pinned version downloads that version (#62).
taxify_download(version = )andtaxify_download_enrichment(version = )fetched the current release and labelled it with the requested version, marked pinned, so the next session never corrected it. The URL is now derived from the release tag (<name>-<version>/<name>.vtr,enrichment-<version>/<name>.vtr), and a version that was never published is an error.taxify_lock()pins the builds a result was produced from (#63). Version and content id are recorded when an enrichment is joined, not read from whatever is installed when the lock is written, so a refresh or restore in between no longer changes the pin. Sources that contributed no value are left out, ascite()already did, andadd_trait()sources carry their version instead ofNA.taxify_restore()reports a row as"unverified"when the lock holds a content id and the install has none, rather than"ok"on matching version labels.Four enrichment join paths now follow the main one (#64). A mixed-grain source (
genus_fallback) fills from the genus row only after cross-backbone recovery has looked for the species, so species resolution wins.groups = "all"reads the groups of the installed build before the manifest, so a pinned or restored build no longer gets columns for groups it lacks. Groupedadd_data()errors on conflicting values within a group, like the flat join, and uses the shared group fill. The emergency grouped path keeps theNAgroup, soadd_common_names(lang = NA)returns names there too.Empty input returns an empty result (#65).
comm2sci(output = "result")with no match, andadd_trait()or anyadd_*()door on a zero-row result, errored or warned once per source. They now return the full output schema at zero rows, built by one constructor.
Five fixes in the verbs around taxify(): browsing the backbone, inspecting a list, and the lookups between names, ids and common names.
No more indistinguishable duplicate rows (#66).
comm2sci(output = "result")returned one identical row per language a vernacular was stored in, because it deduplicated on a key that includedlangand then droppedlang; it now has one row per (query, scientific name).synonyms()repeated every synonym once per copy of a repeated input name; each distinct name is now reported once.inspect()no longer flags valid names or input (#67). The register check read the genus as the raw, case-sensitive first word, so"quercus robur"and"cf. Quercus robur"came backunknown/unresolved; the genus is now taken from the cleaned name and looked up case-insensitively, astaxify()does. A mistyped region code ("GRE"for Greece, which isGRC) was kept as a region, andinspect()then flagged every plant with WCVP data asgeographic.region =now drops a code the WGSRPD crosswalk does not list, with a warning, as it already did for an unrecognised name;inspect()reports the geographic check undernot checked.regionandcoordsare no longer resolved when nothing reads them (a character vector inspected without matching), which could download the region boundaries.Four input shapes in the secondary verbs (#68).
lookup_genus(NA)returned every register row filled withNA; it returnsNULL.id2name()matched a numeric id throughas.character(), which writes1e+05, so a round GBIF key or TSN read from a CSV found nothing; numeric ids are now written out in full, and a fractional one is an error.class2tree()’s$phylotip labels readQuercus_roburwhere$tip_labelsreadQuercus robur; thephylonow carries the names, and only the Newick string uses_.taxify_long()stopped with “No suffixed columns found” on single-group input that carried a companion column such as<trait>_sources, and matched base names as regular expressions in one of its two checks; one literal predicate now decides both.children()anddownstream()are one query (#69).children()isdownstream()with the parent restricted to a genus or family, so the two no longer disagree: underrank = "any"/downto = "any"neither returns the parent itself. Both takekingdom =, astaxify()does, for a name used in more than one kingdom: on COL or GBIF,children("Morus")returned mulberries and gannets together, anddownstream()chose the parent’s rank by a vote across the homonyms. Withoutkingdom, a result that mixes kingdoms now warns. Both return the same columns, addingkingdom_group, andchildren()gainsparent.The sibling verbs forward matching arguments the same way (#70).
synonyms(),upstream(),sci2comm(),comm2sci(),reconcile(),lowest_common()andclass2tree()pass named arguments in...on totaxify(), sofuzzy = FALSE,fuzzy_threshold,kingdomorregionreach the matcher from each of them;synonyms(),upstream()andsci2comm()no longer setfuzzy = TRUEregardless.resolve =now means one thing: run the input throughtaxify()first, as insci2comm().comm2sci()’s argument, which instead changed the return type, is renamedoutput = c("lookup", "result").The eight genus-keyed doors reach every genus their source covers (#77).
add_blanchard(),add_cefas_btrait(),add_disperse(),add_freshwater_insects_conus(),add_fungal_traits(),add_fungalroot(),add_noddb()andadd_ramond()read assets rebuilt by taxifydb 0.1.23. Their cross-backbone name expansion had keyed rows under subgenus renderings and species, which no genus can match, and had pulled in genus homonyms from the other nomenclature code. For fungalroot, fungal_traits and noddb the expanded names also never reached thegenuscolumn the door joins on.list_enrichments()reports every column an enrichment attaches (#76). The manifest’strait_colshad fallen behind 40 of the assets, missing the_min/_max/_nspread columns, FishBase’s and SeaLifeBase’slw_*length-weight columns,bet‘ssubstrateandarthropod_traits’taxon_order. They are now taken from each asset’s schema, and the manifest sync keeps them that way.normalize_kingdom_group()andbackbone_fixed_kingdom()are exported as low-level building blocks, for taxifydb’s kingdom check on enrichment name expansion.GIFT is no longer a suggested package.
add_gift()has read the bundled.vtrsince GIFT was built into an enrichment, and the enrichments vignette no longer describes the earlier on-demand API route.A backbone upgrade patches through xdelta3 when the data directory contains a space (#79).
download_backbone()passed the store paths toxdelta3unquoted, so a path likeC:\Users\First Last\...split into several arguments, xdelta3 exited with “too many filenames”, and every upgrade fell back to a full download (about 2 GB forcol). The paths are now quoted, the patch is fetched through the same route as the full asset (so afile://manifest can serve it), and a failed patch names its reason whenverbose.install_backbones()refreshes an installed backbone the manifest has moved past (#82). It resolved each name throughensure_backbone(), which returns whatever.vtris on disk, so an installed backbone was never compared and only a latertaxify()call fetched the new release. It now runs the same comparison as the once-per-session check intaxify()(one sharedrefresh_backbone()), downloads where the installed build is behind, leaves a current one alone, and keeps a build pinned bytaxify_restore()with a message saying so.A backbone patch is applied only to the build it was cut against (#83).
download_backbone()tried the xdelta3 patch against any local.vtr, and against a different build xdelta3 stopped with “target window checksum mismatch”, printed its own stderr and fell back to a full download thatverbose = FALSEdid not report. The manifest now records the base build’s content id asdelta_from_content_id(written by taxifydb), and the patch is fetched only when the local build carries it; a delta recorded without one is not applied. xdelta3’s output is captured into the reason. Whichever path ran, the new file is checked against the manifestcontent_idbefore it replaces the installed build: a patched file that does not match is replaced by the full asset, and a full download that does not match stops with an error and leaves the previous build in place.meta.jsonrecordsinstall_path("patched"or"full"), and the ready message naming the path and any reason the patch was not used is shown regardless ofverbose.Installing a backbone or enrichment no longer leaves the replaced file’s vectra indexes beside the new one. A download renamed the new
.vtrinto place and kept<name>.vtr.<column>.vtri, and vectra accepts an index whose row and row-group counts match the store, whatever its content (gcol33/vectra#13): a re-cut of the same shape would have pruned by the old keys and dropped exact matches without an error. Every install path (backbone and enrichment downloads, sidecar extras, builds fetched by content id) removes the indexes of the file it replaces, and activating a stored build drops any index the archived build left in the slot. The published assets carry no index, so an installed asset now has the same files on every machine.taxify_pin()pins or releases installed backbones and enrichments by name. A pin was reachable only by fetching a build by content id, so a project building against a shared data directory had no call that held the builds it had already installed, and set"pinned": trueinmeta.jsonby hand. The pin is still thatmeta.jsonflag, now written together with the build’s content id and reported back; the version checks intaxify(),install_backbones()and the enrichment doors leave a pinned build in place. It refuses a name that is not installed, and a name installed as both a backbone and an enrichment (wcvp) untilkind =says which.taxify_restore(install = TRUE)pins every build that matches the lock, including one that already matched and needed no download. It pinned only the builds it fetched, so a lock that matched the install reported success and left those builds free to be refreshed away at the next session. The report gains apinnedcolumn.A name the backbone holds without placing it no longer reads as accepted (#81). The result carries the matched record’s own status in a new
taxonomic_statuscolumn (WFO’s"UNCHECKED", COL’s"PROVISIONALLY ACCEPTED", …). Where such a record’s basionym is placed, the name resolves to the taxon the basionym is placed under, withmatch_type = "basionym"andis_synonym = TRUE: against WFO 2024-12, Sabulina tenuifolia now resolves to Minuartia hybrida. This reads the backbone’s basionym link (original_name_usage_id), which WFO and COL builds carry once rebuilt; a build without it is matched as before. And where one record of a name is a synonym homotypic with its accepted name and another is unplaced, the homotypic record is picked and the unplaced homonym stays inambiguous_targets: Lycopsis orientalis resolves to Anchusa arvensis subsp. orientalis rather than to the unplaced Lycopsis orientalis Steph.
taxify 0.5.2
An audit for the #55 shape – a failure returned as a plausible answer, with the only notice gated on verbose – found it in three more places. The cause behind #55 itself, a cache key handed to an environment, exists nowhere else: every other session cache is keyed on a backbone name or a file basename.
region =andcoords =now warn when the constraint could not be applied. With no range data for the declared region,region_range_sets()returnedNULLand the fuzzy filter passed its candidates through unconstrained, so a match the region would have excluded could win. This is the one that could change which taxon a name resolved to, and the only notice was a message gated onverbose.inspect()names the checks it could not run, in the report header undernot checked. An anomaly-only report has no other way to separate a check that ran clean from one that never ran: both contribute no rows, and the report then prints “nothing stood out”. The genus-recognition check without the register, and both range checks without the WCVP range data, were skipped that way. The skipped set travels on the result, so a saved report carries it.Cross-backbone name recovery names a backbone it could not resolve against, instead of silently contributing no alternatives for it. That pass runs over the whole gap set at once, which is where a size-dependent failure like #55 would next hide.
taxify 0.5.1
add_trait()no longer returnsNAfor every source past a batch size (#55). The per-name-set memos behind hybrid-parent resolution and cross-backbone name recovery were keyed on the query set itself, and R caps the name of a variable in an environment at 10000 bytes – so a large enough batch pushed the key past the cap, where reading the cache raised rather than missed and took the whole join down with it. Because the cap is on total bytes, the batch size that tripped it moved with how many names the backbone resolved and how long they were, which is why a large call could come back with one source populated and the rest empty and read as a coverage result. Both memos now use a session store that carries keys of any length.A trait source that cannot be joined is named in a warning whether or not
verbose = TRUE. Such a source leaves an all-NA column, which reads exactly like the source genuinely holding nothing for those taxa, so silence there turns a failure into a coverage statement.add_trait(verbose = TRUE)reports what each source supplied against the number of names the backbone resolved (gift 152, austraits 40, leda 281, baseflor 336, brot 0 (of 2400 resolved names)), which separates thin coverage from a source that returned nothing.
taxify 0.5.0
CRAN release: 2026-09-02
A content id recorded by
taxify_lock()now resolves back to bytes (#54).taxify_download_enrichment()andtaxify_download()take acontent_id=, fetch that exact build from the immutable copy published beside the rolling asset – reading the manifest’scontent_url, or deriving<tag>/<name>-<content_id>.vtrwhen the manifest describes a later build – verify the bytes hash back to the id asked for, and make it the active build. A build the release has since re-cut is reachable again; one overwritten before taxifydb began publishing immutable copies is not, and says so.taxify_restore(file, install = TRUE)puts a whole lockfile back in one call, fetching the pinned build of every asset that drifted. A restored build is pinned, so the next session’s version check does not refresh it away.Enrichments now keep the build a refresh replaces, under a directory named for its content id, instead of overwriting the only copy on disk. A re-cut under an unchanged tag no longer destroys what a lockfile pinned. Backbones use the same store but discard the superseded build by default, being gigabytes;
taxify.keep_enrichment_versionsandtaxify.keep_backbone_versionsset either policy. Restoring a pinned build archives the build it displaces either way, so a restore is reversible.New
taxify_store()lists the builds on disk – which are active, which are pinned, and which content ids a lockfile could be restored to without a download.
taxify 0.4.9
taxify()no longer resolves a homonym to a synonym of a different species without saying so. Two things drove it (#53). The status score read onlyACCEPTED, so WFO’sUNCHECKED– the status it gives a name it keeps as its own concept but has not reviewed – scored level with a synonym, andPrunus dulciscame back as Prunus avium; status is now an ordered vocabulary (accepted, then unreviewed or provisional, then synonym, then misapplied) that also places COL’sPROVISIONALLY ACCEPTED,AMBIGUOUS SYNONYMandMISAPPLIED, and falls through to the backbone’s ownis_synonymflag for a spelling it does not list. And thenomenclaturalStatus = "Valid"tiebreak was settling the ambiguity tier, soRubus laciniatusreturned Rubus ulmifolius withis_ambiguous = FALSE; validity now orders which candidate the scalar columns hold but no longer clears the flag, because a valid name and an illegitimate one carrying the same string can be synonyms of different species. Both names now answer as they should, andtaxify_candidates()expands the conflict.An authorship carried by the query now settles a homonym whether or not the row was flagged ambiguous: an author the caller typed is a choice of record, and a name unique in the backbone re-reads to what it already had. The comparison also moved from substring containment to author tokens, so
L.no longer reads intoLindl..New
candidate_order()gives the candidate sort a single source of truth, shared withtaxifydb’s name-lookup build.
taxify 0.4.8
Enrichment joins now recover an infraspecific name across GBIF’s dropped rank marker. GBIF’s backbone renders an infraspecific accepted name without its rank connecting term – Erica tenella var. tenella comes out
Erica tenella tenella– where every other backbone and every botanical source keeps the marker, so a join keyed on the rendered name missed in either direction.taxifydbreinstates the marker at build time (gcol33/taxifydb#45), but a user still on an oldergbif.vtrholds the marker-less form, so the runtime now retries the rows a direct (and cross-backbone) join left empty under the name’s alternative rank-marker renderings – the bare trinomial and each canonical marker – and matches when the marker is the sole difference. A rank-insensitive comparison can collide distinct taxa (Aus bus var. cus and Aus bus subsp. cus are different names), so a row whose alternatives reach more than one accepted name in the source is leftNArather than resolved to a guess. Zoological trinomials (Panthera leo persica) are correctly marker-less and match directly, so they never reach the fallback. Only still-empty rows pay for the pass; disable it withoptions(taxify.infra_marker_recovery = FALSE)(gcol33/taxifydb#45).Enrichment joins now recover a name the source covers under a different backbone’s accepted name. An enrichment
.vtris keyed on its source’s own accepted names, expanded at build time onto every backbone’s treatment of the same concept, and where that expansion missed one the join found nothing along the same code path, and with the same empty output, as a name the source genuinely does not cover. Minuartia hybrida was the reported case: WFO keeps the name, WCVP, COL and Euro+Med have all moved it to Sabulina, andadd_wcvp()returned no range at all when matched through WFO. Sampling names from an enrichment’s own.vtrputs the gap at 4.5% to 22.5% of names depending on the enrichment. Names left empty by the direct join are now re-resolved through the other installed backbones, exact matching only, and the join retried under their accepted names, so the WFO route returns the 49 regions the WCVP route does. Where backbones disagree about where a moved name went (COL puts it at Sabulina tenuifolia subsp. hybrida, WCVP at subsp. tenuifolia, and WCVP gives the two different ranges) the pick follows backbone priority rather than a vote, the wayaccepted_nameitself is picked, except that a source taxify also carries as a backbone goes first. Recovery reports once per call, never once per name, since a name absent from a source is the normal case for any query set wider than the source’s scope;summary()and thetaxify_metaattribute (n_recovered) carry the count. Only unmatched rows pay for the pass and backbones scoped to a kingdom none of them belong to are skipped; disable it withoptions(taxify.cross_backbone_recovery = FALSE)(#52).A rebuilt enrichment asset republished under the release tag it already carries now refreshes the local cache.
check_enrichment_version()reconciled content ids only for a static enrichment; a versioned one compared its recorded version against the manifest’s latest, and a same-tag republish leaves that label untouched while the bytes change underneath it. GRIIS 2026.08 was rebuilt after its GBIF-backed source stopped returningrecordidand republished under the tag it already held, so the pre-rebuild.vtrstayed in place indefinitely andadd_griis(cols = "recordid")went on resolving a column the source no longer carries instead of raising the unknown-column error. Content identity now decides and the version string is the fallback, the order the backbone gate already used; a cache predating content ids is still left to the label rather than rehashed every session.The bundled manifest points at the rebuilt
enrichment-2026.08assets and the republishedgbif-2026.08backbone. Every enrichment asset was rebuilt with the build-side reverse hop behind the recovery pass above and carries aresolved_backbonesfield naming the backbones it was expanded against; downloads grow with the wider key sets, 1.96x over the assets with a shipped counterpart (wcvp69 to 141 MB,gift20 to 40 MB).avonetandglonafeach drop atrait_colsentry naming a column their.vtrnever carried, andgriisdropsrecordid, which its upstream source stopped returning (gcol33/taxifydb#44).
taxify 0.4.7
- Grouped enrichment joins no longer discard a name’s data when the routing backbone and the enrichment source spell its authorship differently. The homonym resolution added in 0.4.6 compared author strings exactly, and the sources genuinely disagree: WFO records Calluna vulgaris as
(L.) Hillwhere WCVP, COL and Euro+Med all say(L.) Hull, so one of the commonest plants in Europe came back with noadd_wcvp()range at all when matched through WFO. Authorship is now compared on a normalised key (case, punctuation,etvs&), then as a spelling variant of the same author (one edit apart, or an abbreviation of the same surname), then on a shared basionym author – sources also credit different people with the same recombination (Glaucium corniculatum is(L.) Curtisin WFO,(L.) Rudolphin WCVP), and epithet plus basionym author pins the same type whoever made the combination. A name that no pass resolves to exactly one concept is still leftNAwith a warning, and a genuine homonym pair with no basionym author – 0.4.6’s Erigeron pulchellus Michx. vs Hoppe & Hornsch. – is untouched by the two new passes (#51).
taxify 0.4.6
Grouped enrichment joins (
enrich_by_group(), behindadd_wcvp(),add_griis()and others) now resolve name-level homonyms instead of guessing. The join key is a bare accepted-name string, so a name covering two distinct concepts in the source – told apart only by authorship – collapsed onto whichever row happened to survive the per-group dedup, regardless of which concepttaxify()had actually matched:add_wcvp()reported Erigeron pulchellus Michx. (eastern North America) as native in Germany, when the matched concept was Erigeron pulchellus Hoppe & Hornsch. ex Bluff & Fingerh., a European native. When the enrichment carries an authorship-like column, a name whose rows disagree on authorship is now resolved againstaccepted_authorship: the uniquely picked concept’s rows are kept, or, when the ambiguity can’t be resolved, every row for that name is dropped (leftNArather than guessed) and a single warning reports the count.kingdom =now trusts a single-kingdom backbone’s own scope ahead of the genus register. WCVP, LCVP, WFO, Euro+Med and Species Fungorum carry nokingdomcolumn of their own, so a query against one of them fell straight to the genus register – a single cross-backbone genus index – which can record a different kingdom for the same genus string as used elsewhere (the botanical and zoological codes are independent, so genus homonyms across kingdoms are not rare).taxify(x, backbone = "wcvp", kingdom = "plants")could reportkingdom_group"animalia"for a genuinely plant-only match. A newfixed_kingdomregistry field pins the backbones whose scope is genuinely unambiguous, and is now consulted ahead of both the backbone’s own kingdom column and the register fallback. AlgaeBase and SeaLifeBase are deliberately left unfixed, since each spans more than one kingdom.fuzzy_thresholdnow accepts the integer raw-edit-count mode its own documentation describes. Any value greater than 1 was rejected outright, becausevectra::fuzzy_join()itself only accepts a normalized distance in (0, 1] – the mode was documented but never implemented end to end. A whole number>= 1now runs the join at the loosest normalized bound and reconstructs the raw edit count from the reportedfuzzy_dist(exactlyraw_edits / max(len_a, len_b)) to filter matches.alien_first_recordsis rebuilt from Seebens et al.’s restructured Darwin Core-style release (record 18759840, source version 4.0), 93,350 rows over 245 countries against 82,684 over 233 before. Every column was renamed upstream, sotrait_colsis rewritten rather than preserved; a newalien_first_record_statuscolumn carries the occurrence status of the record each row’s year was taken from.glonafis rebuilt after a wrong join key left it holding 10,238 rows over 84 regions, some names paired with other taxa’s families. Rebuilt from GloNAF v2.02 (Zenodo record 17105725) at 300,094 rows over all 1,343 regions.thermofresh’s citation now points at the peer-reviewed release (record 16959762, ThermoFresh v1.0) rather than the pre-review deposit the build had been reading since taxifydb bumped the source.
taxify 0.4.5
A backbone whose content is unchanged is no longer re-downloaded when its version string moves.
check_version()compared the version label first and only consulted the content id when the labels matched, so it could not see that a bumped version carried bytes already on disk. Content decides now, and the label is the fallback for a cache or manifest entry carrying no id. This matters because a build stampsdate +%Y.%m: a backbone reading a pinned source (Euro+Med from a frozen snapshot, WFO from a fixed Zenodo record) rebuilds to identical bytes under a new version, which used to cost every user a full refetch – 740 MB for WFO. A genuine change is still caught by the same comparison, as is a same-tag republish that leaves the label alone.The multi-backbone fallback 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 name absent from an early backbone but present in a later one now resolves to the later backbone’s exact record. Previously the chain stopped at the first backbone returning any match at all, and a near neighbour in an early backbone settled the name:
taxify("Acilius sulcatus", backbone = c("col", "gbif"))returned the weevilAcalles sulcatusfrom COL, where GBIF holds the diving beetle under the queried name itself. Backbone priority still decides between two matches of the same quality. Over 481 names spanning eighteen backbones, 15 results change, every one of them a fuzzy match in COL becoming an exact match elsewhere, and three of those land in a different family. Runs that name a single backbone are unaffected.mode = "wide"andmode = "agreement"stage their baseaccepted_namethe same way, so it keeps agreeing withmode = "fallback"; their per-backbone columns report each backbone’s own treatment as before.The covered-genus union behind the out-of-scope filter is cached per coverage file and backbone set. It was rebuilt on every matching stage, over roughly half a million genera, which made it the largest single cost of a multi-name run. The same 481 names now take 79 seconds where they took 114.
Five sources open, each reaching something the bundled set did not.
add_hydraulics()brings the drought-mortality axis for 2,024 seed plants (Sanchez-Martinez et al. 2020).add_noddb()brings root-nodule nitrogen fixation for 824 plant genera, joined on genus, which makes it the counterpart toadd_fungalroot()’s mycorrhizal type: together they are the belowground symbiosis pair.add_faprotax()brings 92 metabolic and ecological function groups over 4,470 prokaryotic taxa.add_virion()brings host-virus association breadth for 4,223 vertebrates, andadd_sworm()the ecological group of 171 earthworms, a taxon with no coverage before.-
p50gains a third source and, with it, its first empirical confirmation. AusTraits and BROT share no species, so the trait rested on their two medians landing together at -3.4 MPa. The Sanchez-Martinez compilation overlaps both and agrees with each at ratio 1.00, while taking coverage from 123 species to- The registry records that this is not corroboration: all three are literature compilations over the same primary studies, and 63% of the values shared with AusTraits are identical to the last digit. It is registered because the duplication is confined to 74 of its 894 species, where the sources rejected on these grounds duplicated nearly everything they carried.
The same overlap measures a unit rather than assuming one. 79 of the 208 species the compilation shares with AusTraits match exactly once multiplied by 1e-4, which establishes that its Huber values are square centimetres of sapwood per square metre of leaf.
New traits:
leaf_specific_conductivity,min_water_potential,hydraulic_safety_margin,nitrogen_fixationandearthworm_ecological_group.climatic_temp_meangains its first plant source.min_water_potentialis kept apart fromturgor_loss_point: the two share 39 species and agree on none of them, a field water potential and a pressure-volume threshold being different quantities.sapwood_conductivityandhuber_valuegain a second source each.virus_richnessand the FAPROTAX function sets stay on their doors rather than enteringadd_trait(). Virus richness tracks how much a host has been studied as much as what infects it, with Homo sapiens at 936 viruses over 633,053 records, sovirus_record_countships beside it to keep that visible.GlobalAnts is documented as unusable rather than left as an open question. Its guidelines gate access behind a per-project application, let a contributor require co-authorship, and state that redistribution is not allowed without each contributor’s permission.
Four genus-keyed doors return data rather than failing.
add_blanchard(),add_cefas_btrait(),add_disperse()andadd_freshwater_insects_conus()join ongenus, but their assets were written with the genus names incanonical_nameand nogenuscolumn for the join to reach. The build now materializes that column from the registry’s own grain declaration, so a source declared at genus rank is written joinable at genus rank.The genus register is rebuilt over all nineteen backbones. Two rebuilds are folded together here. The first took it from the thirteen the published build covered to eighteen, so mammals, birds, prokaryotes, algae and fungi reached it through more than the general aggregators; the second adds COL XR, which was wired after that build ran. 496,127 genera become 503,262, coverage rows 1,421,182 become 1,871,955, and the share carrying a kingdom rises from 75.55% to 91.88%.
A backbone the coverage table does not carry no longer has every name in it called out of scope.
covered_genera_for()returned an empty vector for such a backbone where the caller guarded onNULL, so “I have no coverage for this backbone” was read as “this backbone covers no genus”: every unmatched name was stampedout_of_scope, and the abbreviated-genus and fuzzy stages were skipped for all of them. COL XR sat in exactly that position between being wired and the register being rebuilt, and it heads the default priority order. The empty case now takes the path a missing coverage file already took, which is to disable the filter and warn once per session. A set of backbones is only as answerable as its least-covered member, so one uncovered backbone disables the filter for the whole set rather than falling back to the others’ union.A genus filed under two kingdoms by one backbone is now read by that backbone’s own majority. Ordering by source priority alone left the winner to whichever row happened to sort first, which gave 93 genera their source’s minority reading and, because a row recording no kingdom stays eligible to fill the ranks below, produced classifications no source holds: Pteropus, 66 species of flying fox, was indexed as a fungus carrying the order Chiroptera. All 93 resolve, and the tiebreak applies only within a source, so a higher-priority backbone still outranks a lower one.
-
Three backbones join the default fallback chain, giving birds, mammals and prokaryotes a domain authority for the first time: AviList (40,572 rows), the Mammal Diversity Database (61,862) and LPSN (44,885). Every other major group already had one, and the cost of the gap was specific to taxify’s design – the default is a first-match chain, deliberately not a consensus vote, so with no authority in the domain band the answer for these groups was whichever aggregator ranked highest.
All three sit in the domain-authority band, ahead of GBIF, ITIS, NCBI and OTT. This changes what
taxify()returns by default for bird, mammal and prokaryote names, which is the intended effect. They were in the backbone registry without a manifest entry, so until now nothing could resolve them at all. Three more ground-beetle sources join
add_chowdhury().add_finand()brings 34 Helsinki species whose body lengths were measured from the specimens the authors caught — small, and carried because it is the only carabid source independent of carabids.org, which every other reachable table inherits its sizes and wing classes from.add_imageomics_neon()brings the first North American ground beetles, 75 species measured from images of pinned NEON specimens, feedingelytra_lengthand a newelytra_width.add_eberswalde()brings a 24-year monitoring record from 13 forest plots: abundance trend, sensitivity to the 72-month SPEI drought index, a feeding guild refined by field observation to name the prey, and Sustek’s humidity scale.Eberswalde’s size, wing class and latitude are surfaced on its door but feed no trait. The deposit’s own README says they are carabids.org verbatim, and the data agree exactly: over the 19 species shared with
add_chowdhury()every size is identical and every wing class matches. Registering them would double-count one lineage.Ground beetles are now covered.
add_chowdhury()attaches traits and national occupancy trends for 382 German carabids from Chowdhury et al. (2025): body length, wing morphology, trophic level, habitat preference, both Red List codes, and the paper’s own modelled two-year occupancy trend. Carabids are one of the most intensively sampled insect groups in ecology, and taxify’s only beetle source until now wassaproxylic, restricted to the deadwood guild.add_trait()gainswing_morph, three-state on purpose. A wing-dimorphic carabid produces both a long-winged and a short-winged form, and which one dominates is the question carabid dispersal ecology asks, sodimorphic(83 of 382 species) is a value of its own rather than a midpoint or a gap. It is kept separate from the bird traitflightless, which asks whether a species can fly at all.body_length,diet_guildandprimary_habitatgain the same source;primary_habitatgains the vocabulary termopen, the modal class for German carabids, in preference to narrowing it tograssland.diet_guildalso gains the NW European arthropod compilation (Logghe et al. 2025), 3,800 species and the first broad arthropod coverage the trait has had, along with the vocabulary termfungivore. It agrees with the German carabid classification on 86.6% of their 292 shared species.Two columns were deliberately left out of the cross-source verb, both documented in
R/trait-registry.Rso the decisions are not relitigated. The Germanred_list_iucnrestates a national assessment in IUCN letters (perfectly diagonal against the German code over all 382 species) and is not a global one, so it stays on the door. Andarthropod_traitsbody_size_mmis a true body length for spiders and beetles but forewing length for Lepidoptera (0.515 of the wingspan across 50 species), with no taxon column to separate them — see gcol33/taxifydb#40.add_trait()gains seventeen traits and widens four, all from enrichment data already bundled: the registry now covers 204 traits across 456 source slots. Nothing needs re-downloading.Fire ecology and regeneration are now reachable.
resprouting(AusTraits and BROT) is the post-fire resprouter / partial / non-resprouter call,serotinythe canopy seed-storage call, andsoil_seed_bankthe persistence class. Worked examples: Quercus suber and Arbutus unedo come back resprouters, Pinus halepensis and Cistus albidus obligate seeders, Banksia serrata serotinous.Plant hydraulics, the drought-mortality axis, is now registered from AusTraits and BROT:
p50(xylem embolism resistance, MPa),turgor_loss_point,sapwood_conductivity,huber_value, plusbark_thickness. Coverage is thin (123 species forp50) and the trait documents that.Bryophytes enter the trait verb for the first time, from Bryophytes of Europe Traits:
bryophyte_life_form(During’s turf / mat / cushion / weft scheme, kept separate from the vascular-plantlife_formfor the same reasonlichen_growth_formis),shoot_lengthandspore_diameter.New from the coral and bird sources:
larval_development_mode(spawner or brooder),substrate_attachment,linear_extension_rate,polyp_retractability, andprimary_habitat(BIRDBASE’s 14-term vocabulary over 13,067 species). Alsoself_compatibilityfrom Tree of Sex.sexual_systemroughly doubles its plant coverage with AusTraitssex_type(87% agreement with Tree of Sex across 572 shared species) and gains bryophytes from BET.flowering_startandflowering_endare no longer Europe-only: AusTraits’ 12-character monthly string now feeds them, read as the longest circular run so a southern-hemisphere season that wraps the year boundary comes back as December to February rather than January to December.diet_guildgains BIRDBASE, listed last so it fills the birds AVONET and EltonTraits miss.soil_seed_bankis the first trait to ship a documented disagreement. BROT and AusTraits classify persistence by different operational definitions and agree on only 67% of their 48 shared species, so the pair carries acaution: the default coalesce reports the most complete source rather than blending them, and explains itself insoil_seed_bank_caution.add_trait()gainsaggregate_trait_fallback, which thehybrids-and-aggregatesvignette has been documenting as a per-call argument since it was written (“turn it off, per call or globally”). Only the global option existed, so the documented call errored withunused argument. The argument now threads through toenrich_simple()and defaults to the same option, so existing behaviour is unchanged. Grain-pinned sources go throughenrich_by_group(), which has no aggregate fallback, and are unaffected either way.Rejections are documented in the registry header rather than left to be rediscovered, including three octocoral columns that carry no information at all –
tentacles_per_polypis 8 for every one of 3,606 species, because eight tentacles is the defining character of Octocorallia.
taxify 0.4.3
Bundled enrichment data now resolves taxonomic names across all fifteen backbones during the build, up from the seven broadest. An enrichment value keyed to a name accepted only by a domain-specific backbone (Euro+Med, Species Fungorum, AlgaeBase, FishBase, SeaLifeBase, Reptile Database, LCVP, or WCVP) now joins a
taxify()result that resolved through that backbone, instead of falling through. The manifest points at the rebuilt enrichment release.Documentation: corrected references that presented WFO as the default backbone. The default is COL, matched first in the fallback chain, and the first
taxify()call installs the default set COL, GBIF, and ITIS.
taxify 0.4.2
Removed
taxify_download_vtr(), the deprecated alias oftaxify_download(). Usetaxify_download().Slimmed the bundled example database. The BIEN and AusTraits example
.vtrfixtures were the full production tables (116k and 35k rows); they now carry only the few dozen species the offline examples andadd_trait()tests use. Installed size drops from ~9.9 MB to ~3.2 MB. The offline examples and test coverage are unchanged.
taxify 0.4.1
-
add_hybrid_info()gainshybrid_parent_1_id/hybrid_parent_2_id: the backbonetaxon_idof the accepted taxon each parent of a hybrid formula resolves to. An unresolved hybrid formula has no single backbone record, so the result’s owntaxon_idisNA; the two component ids now travel in these columns, each ready to pass straight toid2name().
taxify 0.4.0
CRAN release: 2026-07-23
Breaking: backend is renamed to backbone throughout the API (issue #24)
Two real concepts had one name. A
backboneis a data source and its.vtrfile; abackendis thetaxify_backendS3 handle that knows how to read one. The name-string layer conflated them –install_backbones(backends =)andbackbone_path(backend)collided inside single lines.backbonenow names the data source and its name string everywhere,backendnames the S3 handle only, andresolve_backend()is the single crossing point between them.This breaks existing code, with no aliases.
taxify(backend = "wfo")is nowtaxify(backbone = "wfo"), and the same forsynonyms(),children(),upstream(),downstream(),id2name(),reconcile(),comm2sci(),sci2comm(),class2tree(),lowest_common()andtaxify_download().install_backbones(backends =)becomesinstall_backbones(backbones =).The
backendoutput column is renamed tobackboneintaxify(),taxify_long(),reconcile(),inspect()andtaxify_register_coverage(). A lockfile’sbackendskey becomesbackbones, so a lockfile written before this release no longer loads.Three other conflations went with it.
comm2sci()andsci2comm()returned the accepted name asscientific_name; it isaccepted_name, the name every other verb uses. Theirquerycolumn andparse_name()’sinputandupstream()’squeryare all the caller’s input string, so they are allinput_namenow.register_enrichment()’ssourceargument is the human-readable description, whichenrich_simple()already calledsource_label; the key isname.bb_pathandvtr_pathnamed the same value; it isvtr_patheverywhere.Not renamed: the
backendskey insideinst/manifest.json, and thebackendcolumn inside the publishedbackend_coverage.vtr. Both are serialized schemas thattaxifydbwrites and taxify only reads, so renaming them is a coordinated release rather than a rename. The runtime translates at the boundary, andtaxify_register_coverage()reportsbackbone.
An example fixture was keyed at the wrong level
-
add_fungal_traits()joins ongenus, but the bundled example database shipped afungal_traits.vtrkeyed oncanonical_namewith nogenuscolumn, so its own documented example errored. The fixture is rebuilt from the released asset at genus grain.
The performance and size figures are measured, and generated from the manifest (issue #19)
The README’s headline “1,862x faster than WorldFlora” had nothing in the repository behind it, and neither did the memory figures in the large-scale vignette. Both are now produced by committed scripts –
scripts/benchmark-worldflora.Randscripts/benchmark-memory.R– which write their runs to JSON alongside package versions and the backbone snapshot. On 1,000 names that each carry a one-character typo, taxify takes 18.8 s against WorldFlora’s 4,192 s, both reading the same Zenodo Darwin Core archive; exact matching is 2.2 s against 17.1 s.The invented console output is gone from the large-scale vignette. The timing chunks show how to measure rather than asserting a number that was never run, and the memory section reports resident set size, which is what a backbone actually costs –
gc()sees only a fraction of it, since vectra reads the.vtrthrough the operating system. Opening a backbone costs 1.8 to 2.2 times its.vtrsize, and a 5,000-name all-fuzzy pass adds 1.3 GB against WFO.Backbone row counts, download sizes and the enrichment count are generated from
inst/manifest.jsonand taxify’s own backbone registry, between HTML comment markers, byscripts/sync-readme-stats.R. The hand-kept copies had drifted up to 4.1x (WFO listed at 400k names against 1.6M, its download at ~120 MB against 797 MB). Amanifest-docsworkflow fails if a block goes stale.Smaller: the
taxify()match-type list in the README was missinghybrid_formula, and the Euro+Med row count in the backends vignette still described the 2020 build.
The genus register is downloaded, not built locally (issue #21)
The register carries the
kingdom_group,taxon_groupandlife_formthattaxify()reports, and it was built on each machine from whichever backbones that machine happened to have installed. Two users running identical code therefore got different labels for the same name.genus_register.vtrandbackend_coverage.vtrare now built bytaxifydbover a fixed backbone set, published, and resolved through the same chain as any other asset: disk, download, then a local build. Every install reads the same 496,127 genera.The build pipeline itself – the thirteen genus extractors, kingdom normalization and inference, the GBIF hierarchy walk, and the family life-form table – moved to
taxifydb(about 1,900 lines).taxify_build_register()remains as an escape hatch that delegates there, andtaxify_download("register")fetches the published pair.The register now lives at
<data_dir>/genus_register/latest/rather than<data_dir>/unified/latest/, so it follows the same versioned layout as every backbone and picks up a new release through the usual version check.
Offline mode (options(taxify.offline = TRUE))
- Confines taxify to what is on disk plus the bundled manifest: no version checks, no downloads, no build-from-source that would fetch a raw dataset. A
file://manifest URL still resolves. The test suite runs under it, which is what makes a run independent of any release published between two runs.
A new asset resolves against a manifest that predates it
- The fetched manifest is served from the default branch, so it lags a release that adds an asset. An entry the remote does not carry now falls back to the copy bundled with the installed package instead of failing to resolve. Where both carry a key the remote still wins, so a version bump reaches an already-installed package as before.
Name cleaning keeps Title-Case and all-caps epithets (issue #8)
-
clean_names()stripped a capitalized specific epithet as if it were an author, so"Quercus Robur"cleaned to"Quercus"and then matched the genus rather than the species. Trailing authorship is now removed by token position: the token after the genus is the specific epithet whatever its case, so Title-Case ("Quercus Robur") and all-caps legacy ("QUERCUS ROBUR") binomials keep their epithet, while a genus followed by an author ("Rosa L.") still reduces to the bare genus. Internal-capital author abbreviations ("Picea abies (L.) H.Karst.","Solidago canadensis A.Gray"), which the previous pattern left in the cleaned name, are removed too.
reconcile() no longer reports case variants as merges (issue #11)
- A name paired with its own case variant (
c("Quercus robur", "QUERCUS ROBUR")) was flagged as a many-to-one merge, because the merge detector grouped on the raw input string while the status comparison used the normalized form. Input distinctness is now measured in the same normalized space, so a case- or spelling-only variant counts as one input; genuine merges are unchanged.
lowest_common() ignores blank rank values (issue #15)
- A backbone that stores
""(rather thanNA) for an unresolved rank could makelowest_common()report the blank as the shared ancestor. Empty rank values are now treated as unresolved, so the most recent common ancestor falls through to the next rank every taxon actually shares.
Reverse verbs default to the installed backbone set (issue #24)
-
synonyms(),children(),downstream(),upstream()andid2name()defaulted to a single hardcoded backbone ("wfo"or"col"), so on a fresh installsynonyms("Quercus robur")triggered a WFO download the default backbone set does not include. They now default tobackend = NULL, which resolves to the highest-priority installed backbone, matchingtaxify(),reconcile()andcomm2sci().
inspect() no longer flags valid congeners as typos (issue #10)
-
near_duplicate_targets()measures the edit-distance and length gates on the epithet, not the whole binomial, so"Carex flava"is no longer flagged as a near-duplicate of"Carex flacca". A genuine one-edit epithet typo still fires.
enrich_simple() surfaces join and column gaps (issue #12)
- An explicitly requested
join_colabsent from the.vtrnow errors instead of silently falling back tocanonical_name(the mechanism behind past all-NA doors), and a mapped column absent from the schema warns, naming the enrichment and column, rather than attaching a silent all-NA column. The diagnostic is gated off the bundled example database.
add_combine() preserves metadata and provenance (issue #13)
- A new
[.taxify_resultmethod carriestaxify_metathrough subsetting, soadd_combine()(and any manual subset) no longer strips it. Reported columns are selected by exact set, so an already-imputed value is never mislabelled as a measurement.
Lockfile and summary robustness (issue #14)
-
summary()no longer errors on an enrichment entry without a version;taxify_restore()reports a distinct"unverified"status when nothing could be compared (instead of a false"ok"), and tolerates zero-length lockfile fields.
Enrichment metadata and example-database safety (issue #16)
-
download_enrichment()copieslicense,available_groupsandcitationinto the installedmeta.json(licence is no longerNAfor downloaded enrichments);ensure_enrichment()refuses to download an unbundled enrichment into the example database. - The bundled manifest now carries
group_colfor all seven grouped enrichments (griis,gidias,alien_first_records,wcvp,marine_distribution,common_names,glonaf), andenrichment_groups()falls back to the manifest when the installedmeta.jsonlacks it. A grouped enrichment installed before the metadata was copied reported itself as ungrouped with no way to recover, becauseavailable_groupshad a manifest fallback andgroup_coldid not.
Fuzzy matching picks the closest candidate (issue #23)
- Candidate ranking had no distance term:
score_candidates()ordered by status, rank, validity, epithet and thentaxonID, so among several fuzzy candidates the winner was decided by ID and a farther name could beat a nearer one."Carex flavaa"resolved toCarex flacca(distance 0.167) whileCarex flava(0.083) was in range, and the two were reported ambiguous although one was strictly closer. Distance now sorts first, so the nearest name wins and ambiguity is declared only among equidistant candidates. Exact matching is unaffected. -
dedup_fuzzy_targets()ran once per fuzzy pass, so a query dropped by the genus-blocked pass competed again in the prefix-blocked fallback and claimed the backbone row the first pass had already taken. The claimed rows are now carried forward, so two queries can no longer collapse onto one row. - Measured over the full 34,589-name EVA-to-WFO corpus, fuzzy accepted-name agreement rises from 0.9152 to 0.9172 and WFO-ID agreement from 0.8031 to 0.8050, against a 0.0009 fall in match rate.
An executable accuracy corpus (issue #23)
-
tests/e2e/test-asaas-validation.Rasserted nothing: it printed its agreement rates withcat(), so a matching regression reported a worse number and passed, and it read from a hardcodedJ:/path. It now locates the corpus throughTAXIFY_ASAAS_CORPUS, asserts floors on match rate and on accepted-name, family, genus and WFO-ID agreement over the whole corpus, and asserts both sides of the fuzzy trade so neither recall nor precision can erode unnoticed. Divergence is split into backbone drift, the corpus hybrid-formula convention, and genuine misses, and only the last is capped. Seetests/e2e/README.mdfor the baseline and how to read a disagreement. - Assertions that could not fail were replaced: the trait-registry vocabulary check drove every crosswalk with
NAonly,test-doors.Raccepted one populated column out of twenty,children()case-insensitivity held at zero rows,summary()was matched withgrepl("3", .), and the multi-backend fallback test used fixtures that both carried the species. Self-fulfilling comparisons intest-default-backend.R,test-add-trait.R,test-region.R,test-gap-verbs.Randtest-new-verbs.Rnow assert independent literals. - New coverage for ITIS (a member of the first-run backbone set that had no test at all),
add_common_names(),add_wcvp(region=),add_glonaf(),add_alien_first_records(),install_backbones(), andtaxify_restore()’sversion_drift/content_drift/missingbranches. Negative and threshold-boundary fuzzy tests cover equidistant candidates, typos that must not resolve to a close-but-wrong species, and the accept/reject flip.
Authorship tiebreak and fuzzy validation (issue #17)
- The authorship homonym tiebreak writes the accepted taxon’s genus/family, not the rejected synonym’s, so a disambiguated homonym carries a consistent lineage and life-form.
fuzzy_joinerrors now warn instead of silently returning no matches, andtaxify()validatesfuzzyandfuzzy_threshold.
Internals: backend registry and shared enrichment join (issues #20, #22)
- The 15
backend-*.Rclones collapse into one registry-driven factory; adding a backbone is now a registry row plus a manifest entry. The three backend-info doors (add_wfo_info()/add_gbif_info()/add_col_info()) share oneenrich_from_backbone()helper, andadd_data()uses a vectorized, typed-NA join keyed onaccepted_id.
taxifydb is required at 0.1.19 or newer
- The register fallback calls
taxifydb::build_register(), which taxifydb added during 0.1.18 rather than at its start, so a 0.1.18 built before that point satisfied an unversioned dependency and then failed the call.Suggestsnow namestaxifydb (>= 0.1.19), the first version whose number identifies code that carries the function.
Documentation (issues #18, #19)
- Every
\donttestexample now runs offline against the example database or is\dontrun, soR CMD check --run-donttestnever triggers a download. UTF-8 BOMs removed from three R files and three vignettes;taxify()’s@returndocuments all 26 columns; the FISHMORPH trait list matches the columnsadd_fishmorph()emits.
The fallback chain reaches every backbone again (issue #9)
A name whose genus the leading backbone did not cover was marked
"out_of_scope"and then skipped by every backbone behind it, because the fallback loop only retried rows whosematch_typewasNA. The chain ended at whichever backbone happened to be first. The package’s own documented example,taxify(c("Quercus robur", "Panthera leo"), backend = c("wfo", "gbif")), returned Panthera leo asout_of_scopewithout ever consulting GBIF.The default
backend = NULLpath was hit hardest, since COL leads the priority order: on a full install 41.4% of register genera (207,980 of 502,088) sit outside COL’s coverage, and every one of them is carried by a backbone further down the chain. 14,354 of those are marine genera that never reached WoRMS, soregion=could not constrain a marine name COL does not list.taxify("Parvocaris samnitica")now resolves through WoRMS instead of returningout_of_scope."out_of_scope"means what the documentation always said it meant: no backbone in the query covers the genus. The per-backbone mark still runs, so the abbreviated-genus and fuzzy stages are still skipped for a backbone that cannot carry the name (which also stops a spurious fuzzy near-match there from outranking the exact match waiting further down the chain), but it is released again for any name another backbone in the chain still covers. A single-backend query is unchanged.backendandbackbone_versionnow name only the rows a backbone lookup actually resolved. They were stamped whenevermatch_typewas non-NA, which swept in the verdicts a backbone never produced: anout_of_scoperow read the leading backend’s name and its resolved version even though no lookup found it there. Selectingresult[result$backend == "wfo", ]then counted those rows, so a per-backbone hit rate over-reported every backbone in the chain."exact","exact_ci","fuzzy"and"abbrev"carry the two columns;"out_of_scope","hybrid_formula"and"none"leave themNA.
The marine region assets ship (issue #21)
0.3.20 added the marine range providers but the data they read did not exist yet, so the marine path stayed inert for everyone. Both assets are now published and registered in the manifest, and
region=/coords=constrain marine matches out of the box.New
meowboundary entry: 196,807 polygon vertices across all 232 Marine Ecoregions of the World, resolved throughdownload_backbone()the waywgsrpdis. Reference geometry, so it does not join the taxonomic backbone set thatlist_backbones()reports.New
marine_distributionenrichment entry: WoRMS distribution records rolled up to MEOW ecoregions, keyed oncanonical_namewith aregion_codeand a native/introduced status, the marine counterpart of the WCVP range table.taxify_regions()lists both vocabulariesregion=accepts, and gains aschemeargument ("all","wgsrpd","meow") and aschemecolumn. It resolved marine names as soon as the asset was installed but only ever listed the botanical crosswalk, and the warning for an unrecognised region pointed users there. MEOW rows reuse the three name columns, since the two schemes nest the same way: ecoregion asname, province aslevel2_name, realm aslevel1_name. Marine rows appear only when the asset is installed, which is also when they resolve.
taxify 0.3.20
Region filtering generalizes beyond plants (issue #21)
region=andcoords=now constrain fuzzy matching for marine taxa, not only vascular plants. When themarine_distributionasset is installed,region_range_sets()unions two providers behind the unchanged arguments: WCVP (plants, keyed on TDWG Level 3tdwg_code) and the marine distribution table (keyed on MEOW ecoregionregion_code). Each resolved region code is routed to its provider by vocabulary – alphabetic TDWG codes to WCVP, numeric MEOWECO_CODEs to the marine table – through one shared engine, so a plant-only query never touches the marine asset and vice versa.region=accepts MEOW ecoregion codes and ecoregion / province / realm names (a province or realm expands to its member ecoregions), resolved from the installed marine asset.coords=maps points to MEOW ecoregions by point-in-polygon against a newmeow.vtrboundary set, unioned with the WGSRPD botanical lookup, so a coastal coordinate can resolve to both a botanical region and a marine ecoregion. The point-in-polygon machinery is now scheme-generic across the botanical and marine boundary sets.Marine support is inert until the
marine_distributionenrichment is installed: the plant-only default path is unchanged.
Five more name verbs
-
parse_name()decomposes a name into its parts – genus, specific epithet, infraspecific rank and epithet, authorship, hybrid status, open-nomenclature qualifier, and the canonical form – without matching against a backbone, the rolergbif::name_parse()or gnparser fill. It reuses the same cleaning pipelinetaxify()runs, so a name breaks apart the way it matches. -
upstream()returns the higher classification (ancestors) of a taxon as a tidy long frame, one row per rank from kingdom down to genus;to =restricts it to a single rank (“what family is this in?”). The ancestor-direction complement ofdownstream(). -
sci2comm()resolves scientific names to their common (vernacular) names – the forward complement ofcomm2sci(), reading the bundledcommon_namesdata offline.resolve = TRUE(default) taxifys the input to its accepted name first. -
reconcile()reports how a checklist maps onto a backbone’s current treatment, classifying each name asunchanged,synonym,misspelling,ambiguous, orunresolved, and flagging many-to-one merges where several inputs collapse onto one accepted name – a checklist-migration summary. -
taxify_lock()/taxify_restore()write and verify a reproducibility lockfile:taxify_lock()serializes the resolved backbones and enrichment layers (name, version, content id, download date) behind a result to JSON, andtaxify_restore()compares a lockfile to the current install and reportsok/version_drift/content_drift/missing.
Authorship-aware homonym disambiguation
- When an ambiguous match (a homonym such as Pinus abies L. vs Thunb.) carries an author in the input,
taxify()now resolves it to the single author-matching accepted target instead of returning the tie. The tiebreak only touches ambiguous author-bearing rows; the fast path and every unambiguous result are unchanged.
Bug fixes
- Resolved an
R CMD checkwarning about an unresolvabletaxifydb::build_meowreference. The scheme’s boundary builder is now looked up by name at run time, so the marine builder is used when taxifydb provides it and skipped cleanly when it does not.
taxify 0.3.19
Five new name-resolution verbs
-
comm2sci()resolves common (vernacular) names to scientific names – the reverse ofadd_common_names(). Reads the bundledcommon_namesdata offline;resolve = TRUEruns the matches throughtaxify()and returns an enrichabletaxify_result. -
id2name()looks a backend ID (a GBIF key, ITIS TSN, WoRMS AphiaID, …) back up to its name, rank, classification, and accepted-name resolution – the inverse of thetaxon_id/accepted_idcolumnstaxify()emits. -
downstream()lists every accepted taxon at a chosen rank (species by default) beneath a higher taxon, wherechildren()gives only the immediate level. -
class2tree()assembles a taxonomy tree (Newick, plus anapephyloobject whenapeis installed) from a set of resolved names, andlowest_common()reports the deepest rank at which a set of names shares a common ancestor.
Cross-kingdom disambiguation on taxify()
-
taxify(..., kingdom = )restricts matches to one or more kingdoms ("animals","plants","fungi", …), so a name shared across kingdoms (a Prunella that is both a bird and a plant) resolves to the intended one. A match whose kingdom is wrong is passed on to the next backbone in the fallback chain; the kingdom is read from the backbone where it stores one, falling back to the genus register otherwise.
taxify 0.3.18
Documentation
- The README now notes that the backbones are pre-built by the companion taxifydb package, and updates the enrichment-layer count to match
list_enrichments().
taxify 0.3.17
A downloaded backbone reports the version it actually contains
- Downloading a backbone into a slot that was previously built from source no longer keeps reporting the old built version.
download_backbone()now clears the stale.metabuild sidecar it leaves behind, restoring the invariant that a downloaded backbone carries only itsmeta.json. Previously the leftover.metashadowedmeta.jsoninformat_backbone_version(), so a freshly downloaded release (e.g. Euro+Med 2026.07) was still labelled with the old built version (2020.1) in thebackbone_versionoutput column.
meta.json and manifest reads tolerate a UTF-8 BOM
- A
meta.jsonor manifest carrying a leading UTF-8 byte-order mark (as written by Windows PowerShell’sSet-Content/Out-File) no longer emits an “illegal byte-order-mark” warning on every version read. All meta.json and manifest reads strip a leading BOM before parsing.
taxify 0.3.16
The genus register is reachable and builds from what you have
taxify_build_register()is now exported. It builds the unified genus register from the backbones you have installed and is the front door for thelife_form/kingdom_group/taxon_groupoutput columns, out-of-scope detection, andinspect()’s no-backbone checks. Previously the only builder was internal, sotaxify_load_register()on a fresh setup pointed users at a function they could not call, and the manifest’s register entry was a Zenodo placeholder that never resolved.taxify_download("register")is a convenience alias for it.Building the register no longer force-downloads every known backbone. It unions only the backbones actually installed (via the new internal
installed_backbone_path()), matching the documented “union of installed backends” behaviour instead of silently pulling several gigabytes.
Backbone version reflects the installed data, not a static default
The build-from-source shims no longer pass a hardcoded version label into
taxifydb::build_<name>(); taxifydb stamps its own current version. A backbone built from source is now labelled with the data it actually contains, not a constant that drifts as releases advance.format_backbone_version()falls back to the download-timemeta.json(the manifest version) before the backend constructor’s static default, so a downloaded backbone reports the version it was downloaded at.Manifest: the
reptiledbbackbone points at thereptiledb-2026.07release (byte-identical to 2026.06) and records its content id; the placeholderregisterentry is removed.
taxify 0.3.15
add_gidias() can ask what a species impacts
GIDIAS scores each impact record against an affected native taxon, so
add_gidias(group = )reads impact at that grain: Felis catus is"MO"against invertebrates (reduced populations) where the default is"MV", the global extinctions it drove among vertebrates. 28% of species with an affected taxon recorded impact two or more of the five groups (Plant,Invertebrate,Vertebrate,Microbe,Fungi), so for those the species-level category is the most severe impact on anything, not the impact on each thing.enrichment_groups("gidias")lists them; several groups suffix the columns, asadd_alien_first_records()does for countries.add_gidias()with nogroupis unchanged: it reads the"Any"grain, the same all-records summary it returned before, with the same columns."Any"is the only grain carrying SEICAT and the impact records with no affected taxon recorded, sogidias_seicat_categoryisNAon the other groups. Requires the rebuiltgidiasenrichment.colsnow works on the grouped doors even when the source carries no columns beyond the curated set, which previously ignored it.
Citations carry an access date
- A source harvested live from a server is not identified by its year: two harvests a year apart share it.
format_bibtex_entry()now emits the citation’snotefield, so an access date recorded in the manifest reaches the BibTeX entry. Entries carrying nonoteare unchanged.
taxify 0.3.14
A wrong temperature reached users, and is fixed
- FishTraits shipped a
-1missing-data code in its two temperature columns, so 90 introduced fish (goldfish, zebrafish, brown trout) reported a July maximum of -1 degrees C, which is impossible in the conterminous US.add_fishtraits()surfaced it asft_min_temp_c/ft_max_temp_c. The rebuilt data reads NA for those species, and the warmest-month value now bottoms out at a believable 20.4 degrees C. Species whose January minimum genuinely is -1 keep it. - The same source also used “.” for an absent text value, which reached
ft_itis_tsnandft_common_name, and produced five unusable rows named after a family (“Percidae .”). Both are cleaned. - The corrected data ships under the existing release, and taxify refreshes a stale local copy once, on its own, the first time you use it.
The range climate gains its minimum and maximum
- New
climatic_temp_minandclimatic_temp_max(degrees C): the coldest-month and warmest-month temperature at a species’ range centroid, from FishTraits. They joinclimatic_temp_meanin the climatic-niche family, which stays apart from the organismalthermal_max/thermal_min. Largemouth bass shows why the split earns its keep: its range tops out at 32.0 degrees C while it can survive 33.5. These were held back until the-1code above was fixed.
Plant chromosome numbers
-
chromosome_numberreported a count nobody had measured, and is fixed. Arabidopsis thaliana read 15 and Acer campestre 39, from species recorded at more than one cytotype: Kew holds a diploid 2n = 10 and a tetraploid 2n = 20 for Arabidopsis, and averaging them lands on 15, which is in neither record. The value is now the base cytotype (10 and 26 for those two), and the range across cytotypes is reported inchromosome_number_min/chromosome_number_max, so nothing is lost. 421 species change value, and every value now comes from a real record. - An odd chromosome number is still reported wherever Kew measured one. A triploid carries an odd 2n by definition (Tahiti lime is 2n = 3x = 27), as do aneuploid hybrids and the gametophytic counts of mosses and liverworts.
- Kew’s plant genome size is a continuous measurement and is unaffected.
- Every Kew value can now be traced to the paper it was measured in.
add_kew_cvalues(cols = "all")attachescval_original_referenceandcval_estimation_method, both populated for every species, covering 941 distinct papers. Where a species carries records from several papers, all are listed, as with Arabidopsis thaliana (“Bennett et al.,2003; Schmuths et al.,2004”). - The estimation method is worth reading before comparing genome sizes. 9,015 species were measured by flow cytometry and 3,391 by Feulgen densitometry, which are different measurements, and 97 species carry both.
- The corrected data ships under the existing release, and taxify refreshes a stale local copy once, on its own, the first time you use it.
- New
add_ccdb(): somatic chromosome numbers from the Chromosome Counts Database for 65,051 plant species, with the range of cytotypes each spans. CCDB states no licence, so taxify publishes no copy; the door builds it on your machine through taxifydb, like the other build-only sources. - The
chromosome_numbertrait continues to come from Kew alone. CCDB covers seven times more species, but a source reachable only where a build tool is installed would makeadd_trait()return a different number on two machines running the same code. - Kew’s plant genome size stays behind
add_kew_cvalues()in picograms rather than joining the prokaryotegenome_sizetrait in base pairs. The picogram to base-pair conversion is a constant, but a plant 1C value counts every subgenome a polyploid carries, while a bacterial genome size counts one chromosome.
taxify 0.3.13
Four sources taxify cannot redistribute now have doors
- New
add_gmpd()(Global Mammal Parasite Database 2.0: per-host parasite richness, counts by parasite type, mean prevalence),add_plantatt()(British and Irish vascular plants: Ellenberg values, maximum height, life-form/woodiness/status codes),add_bryoatt()(the bryophyte companion – a group otherwise almost absent from the bundled traits), andadd_clopla()(CLO-PLA clonal and bud-bank traits of the Central European flora, 29 codes kept verbatim). - These four state no usable licence, so taxify publishes no copy of them and they carry no manifest entry. The door builds the data from the original source on your own machine through taxifydb, which is required for them and errors with an install instruction when absent. taxify redistributes none of the data; cite the sources when you use them.
Climatic niche is now separate from thermal tolerance
- New
climatic_temp_mean: the mean annual temperature of a species’ range, in degrees C, from the NW European arthropod dataset (WorldClim BIO1 over occurrence buffers) and ReptTraits (CHELSA over GARD ranges). It is kept strictly apart fromthermal_max/thermal_min, which are organismal CTmax and CTmin: a range climate is bounded by dispersal, competition and history, so it sits well inside what an animal can survive.
New traits
-
forearm_length(mm): the standard bat measurement, from COMBINE, PanTHERIA and the European bat dataset (the first two carried it for 1130 and 1012 species without any trait reaching it). -
chromosome_number(2n): from the Kew Plant DNA C-values database. -
thermal_max/thermal_mingain the freshwater thermal-tolerance database (CTmax and CTmin of freshwater fish, invertebrates and amphibians). -
body_mass,longevity,clutch_litter_sizeanddiet_guildgain the European bat dataset;longevityandage_at_maturitygain FishTraits;clutch_litter_sizegains the zooplankton per-clutch egg count.
taxify 0.3.12
Default backend is now every installed backbone
-
taxify(backend = NULL)(the new default) matches against every installed backbone as a first-match fallback chain, instead of only WFO. A name is resolved by the first backbone that matches it in priority order – COL, then the domain authorities (marine, vascular plants, fungi, algae, fishes, reptiles), then the broad aggregators (GBIF, ITIS, NCBI, OTT) – so cross- kingdom input works without knowing which backbone to name, and disagreement cases resolve to the more current treatment (e.g.Macropus rufus->Osphranter rufus). It is a first-match pick, not a consensus vote (a vote would regress toward the most conservative treatment across backbones that copy one another); usemode = "wide"/"agreement"to see disagreement. - On a fresh setup with nothing installed, the first
taxify()call downloads a default set once – COL, GBIF, and ITIS – covering all kingdoms with a genuine cross-backbone agreement signal. Pre-install a different set with the newinstall_backbones(), or set it viaoptions(taxify.default_backbones = ...). The priority order is configurable withoptions(taxify.backbone_priority = ...). - New exported
install_backbones()downloads one or more backbones for offline use (defaults to the first-run set). - Naming a
backendexplicitly is unchanged, so existing calls keep working; only the unset default moved off WFO.
COMBINE: one door fills imputed gaps and tags provenance
-
add_combine()is now the full COMBINE door rather than a thin alias ofadd_combine_reported(). It attaches the reported (measured) values and fills each still-missing cell from COMBINE’s phylogenetically imputed table, so a single call reaches COMBINE’s fullest coverage. A measurement is never overwritten – the reported value wins and the model only fills gaps – and a companioncombine_<trait>_srccolumn tags every cell as"reported","imputed", orNA, keeping the measured-vs-modelled distinction explicit per cell.add_combine_reported()(measured only) andadd_combine_imputed()(the imputed table on its own) are unchanged. Runtime-only; no data rebuild.
taxify 0.3.11
Genus-resolved enrichment joins
-
add_cefas_btrait()now joins ongenus. The Cefas benthic-traits database codes traits at genus level, so the previous species-level join returnedNAfor every species; it now annotates every species in a coded genus. -
add_epa_freshwater()now uses a species-first, genus-fallback join. The US EPA Freshwater Biological Traits Database records each trait at the finest resolution available, so a species keeps its species-level values and any trait still missing is filled from the taxon’s genus-level row (a species value is never overwritten). This is a newgenus_fallbackoption on the shared enrichment engine, available to any mixed-resolution source.
taxify 0.3.10
Five more trait sources
- Five enrichments that had working parsers but had never been released now have doors:
add_kew_cvalues()(plant genome size, chromosome number and ploidy from the Kew Plant DNA C-values database, CC BY),add_copepod_traits()(marine copepod body size, egg and clutch traits, Brun et al. 2017, CC BY 3.0),add_fishtraits()(United States freshwater-fish life history, temperature and salinity tolerance, and conservation traits, Frimpong & Angermeier 2009, public domain),add_epa_freshwater()(freshwater-invertebrate functional traits, US EPA, public domain), andadd_cefas_btrait()(North-West European shelf benthic biological traits, Cefas, OGL v3.0). Each is source-prefixed (cval_,cop_,ft_,epa_,cefas_) and takescols = "all"for the full column set.
Documentation and metadata
- The reference index now lists
add_combine_reported(),add_combine_imputed(),enrichment_groups(), and the low-level building blocks. -
thermofresh,ramond,freshwater_insects_conus, andeurobatare flagged static in the manifest, so their version is not re-checked each session.
taxify 0.3.9
Invasion impact (EICAT / SEICAT)
add_gidias()joins per-species invasion-impact aggregates from GIDIAS (Bacher et al. 2025, CC BY 4.0), the IPBES invasive-species assessment’s global impact compilation. Each species carries its IUCN EICAT environmental-impact category (gidias_eicat_category,MC/MN/MO/MR/MV, orDD) and SEICAT socio-economic-impact category (gidias_seicat_category), each the most severe magnitude among the species’ documented negative impacts, alongside the driving mechanism, affected well-being constituents, realms, and record and source counts. Only the derived per-species aggregates are distributed, not the raw impact records.add_trait()gains two categorical traits,"environmental_impact"(EICAT) and"socioeconomic_impact"(SEICAT), so invasion impact reconciles across sources the way every other trait does. GIDIAS is the first source; the ordinal categories are shared so further EICAT assessments can coalesce onto them later.
taxify 0.3.8
Economic cost of invasions
-
add_invacost()joins per-species economic-cost aggregates from InvaCost (Diagne et al. 2020, CC BY 4.0):invacost_cost_total_usd(cumulative documented 2017-USD cost),invacost_cost_n(number of estimates), and the dominantinvacost_cost_type.
PHYLACINE mass is not silently model-derived
-
add_phylacine()surfacesphylacine_mass_methodandphylacine_mass_method_class, recording whether a body mass is measured (reported), allometricallyestimated, or phylogeneticallyimputed. -
add_trait("body_mass")now flags model-derived PHYLACINE masses per species: where PHYLACINE contributes an imputed or estimated mass,body_mass_cautionexplains it for that species only; measured masses are left unflagged. A trait registry source can attach such a per-record caution viacaution_col/caution_fn, distinct from the existing whole-source method caution.
taxify 0.3.7
Compare backbones in one call
-
taxify()gains amodeargument. With more than onebackend, the defaultmode = "fallback"is the usual fallback chain, whilemode = "wide"consults every backbone for every name and adds oneaccepted_<backbone>column plus a logicalall_agree, andmode = "agreement"addsn_backbones_matched,n_distinct_accepted, andall_agree. Both are a strict superset of the standard result (a singleaccepted_name, still pipeable into theadd_*()enrichments), so a backbone disagreement, such as GBIF keepingMacropus rufuswhere the Catalogue of Life resolves it toOsphranter rufus, is visible in one call instead of querying each backbone by hand.
Discovering group values
-
enrichment_groups()lists the group values a grouped enrichment door can filter on, the wayenrichment_cols()lists a door’s columns. It answers “what country / region / language codes are valid?” foradd_griis(country=),add_wcvp(region=),add_common_names(lang=), andadd_alien_first_records(country=).
Aggregate trait resolution
An aggregate query (
"Rubus fruticosus agg.","... s.l.") now falls back to the nominal binomial’s trait when a source carries no aggregate-level value. Aggregate-level values are still used first where present; the binomial is a pragmatic stand-in, not a real aggregate-level measurement. This also fixes a preserve-fell-back aggregate (one with no dedicated aggregate taxon in the backbone) reaching its binomial’s traits.The fallback is on by default and can be turned off per call with
aggregate_trait_fallback = FALSE, or globally withoptions(taxify.aggregate_trait_fallback = FALSE), which keeps a resolved aggregate without aggregate-level data asNA.options(taxify.trait_provenance = TRUE)now adds a character<enrichment>_basiscolumn ("primary"a same-level hit,"aggregate"a species inheriting its aggregate’s value,"binomial"an aggregate standing in on its binomial) in place of the earlier logical<enrichment>_inheritedflag, so the basis of every filled value is explicit.
taxify 0.3.6
Hybrid matching
Hybrid names are handled by type instead of being reduced to their first component.
taxify()gains ahybrid_typecolumn ("nothogenus","nothospecies","formula", orNA).Hybrid formulas (
"Salix alba × Salix fragilis") no longer resolve to a single species. Previously the cross was silently cut to its first parent and returned as an exact match toSalix alba; now the cross row carriesmatch_type = "hybrid_formula"withNAbackbone-match columns. The parent binomials, and their accepted names, are available on demand viaadd_hybrid_info(), which now addshybrid_parent_1,hybrid_parent_2,hybrid_parent_1_accepted, andhybrid_parent_2_accepted.The same-genus formula shorthand is recognized:
"Salix alba × fragilis"(second parent written as a bare epithet) parses to parents"Salix alba"and"Salix fragilis".Nothogenus names match backbones that store the multiplication sign.
"× Cupressocyparis leylandii"previously returnedNAagainst backbones whose canonical form keeps the sign (e.g."× Cupressocyparis leylandii"); matching now tries the"× Genus ..."and"×Genus ..."forms alongside the sign-stripped binomial. Nothospecies ("Quercus × hispanica") continue to match as before.
taxify 0.3.5
Discovery
New
list_backbones()lists every supported taxonomic backbone with its scope, name count, download size, version, install status, and source, mirroringlist_enrichments(). Newtaxify_databases()stacks the backbones and enrichments into one overview so the full breadth is one call away. A canonical backbone registry now backsresolve_backend(),installed_backbones(), and these verbs, so the supported set cannot drift between them.A “Databases” pkgdown article lists every backbone, enrichment, and trait, generated from the shipped manifest so it stays current.
taxify 0.3.4
CRAN release: 2026-07-09
Bug fixes
-
clean_names()recognizes a much wider set of taxonomic qualifiers and rank abbreviations. Previously an unrecognized marker survived into the cleaned name, so the name no longer matched the backbone’s bare binomial/trinomial (it dropped to fuzzy) and the annotation was lost from thequalifiercolumn. Now folded to their canonical tokens: the subspecies abbreviationsssp.andnssp.and the spelled-outsubspecies(tosubsp.); the sensu-stricto forms.s.(tos.str.); the forma spellingsfo.andforma(tof.); the infraspecific rankssubvar.,subf., andconvar.; the notho- (hybrid) ranksnothosubsp.andnothovar.(tosubsp./var., the hybrid signal being carried separately by the multiplication sign); cultivarcv.; the pathogen infrasubspecific categoriesf. sp.(forma specialis, previously mislabelled as a bare forma) andpv.(pathovar); the determination markernr.(“near”); the open-nomenclature markersindet.andsp. nov.; the long and bare concept formss. lat.andcoll.(tos.l.); and the species-group markersgroupandgr.. Each token matches only as a whole trailing word, so it never touches a real epithet (Carex novae-zelandiaeandConvallaria majalisare untouched). The qualifier registry inR/clean.Rremains the single source of truth for every spelling.
taxify 0.3.3
New features
- WCVP and LCVP join the plant backbones.
taxify(x, backend = "wcvp")matches against Kew’s World Checklist of Vascular Plants (Govaerts et al. 2021, ~1.4M names, CC BY 4.0);backend = "lcvp"matches the Leipzig Catalogue of Vascular Plants (Freiberg et al. 2020, MIT). Both slot into the fallback chain (backend = c("wcvp", "wfo")) and contribute their genera (kingdom Plantae) to the unified genus register. Fifteen backbones are now available. - Four new verbs read the backbone in the directions
taxify()does not.synonyms()lists every synonym that resolves to a name’s accepted taxon (the reverse of the forward resolution).add_classification()fills the higher ranks (kingdom, phylum, class, order) from the matched backbone, complementing thefamilyandgenusalready in the core output.children()lists the accepted taxa within a genus or family, for building a checklist rather than only validating one.taxify_candidates()expands an ambiguous match (is_ambiguous) into one row per candidate accepted taxon, so homonyms can be resolved by hand. -
taxify()gains anaggregate_fallbackcolumn that makes the defaultaggregates = "preserve"accountable. Where a backbone carries no dedicated aggregate taxon for an aggregate query, preserve falls back to the nominal binomial; that collapse is now flagged (TRUEon fallback,FALSEwhen the aggregate taxon was matched,NAfor non-aggregate queries and undercollapse) rather than being silent. Only Euro+Med (433 aggregate taxa) and WoRMS (189) carry them, so preserve is a no-op for the other backbones – the flag now says so per row. -
add_kew_sid()opens the Kew Seed Information Database (SER-SID, CC BY 2.0): seed weight (thousand-seed weight over 42,000 species), storage behaviour (orthodox/recalcitrant/intermediate), oil and protein content, life form, and fruit type, joined onaccepted_nameacross 50,146 accepted names. Verified end-to-end (Quercus robur 3493 g/1000 seeds and recalcitrant; Helianthus annuus 43.5% seed oil). Its seed weight also joins the cross-sourceadd_trait("seed_mass")verb as a seventh source (a thousand-seed weight in grams equals the per-seed mass in mg; grounded at ratio 1.00 against Diaz, BIEN, and AusTraits), roughly doubling seed-mass species coverage. -
add_edwards_phyto()opens the Edwards et al. (2015) phytoplankton nutrient-utilization database (~130 species): Droop/Monod uptake and growth parameters for ammonium, nitrate, and phosphorus (mu/k/vmax/qmin/qmax), plus cell volume, carbon content, taxonomic group, and marine/freshwater habitat. A curated core attaches by default;cols = "all"surfaces every uptake parameter. Door-only (single-source physiology with no cross-source analogue), so it stays out of theadd_trait()verb. Verified end-to-end (Alexandrium catenella recovered as a marine dinoflagellate). - Nine new
add_trait()traits covering algae, marine-benthic invertebrates, and octocorals, each grounded on its source’s own vocabulary before wiring. Algae (AlgaeTraits):calcification,gamete_type,algal_life_cycle(dominant ploidy phase),algal_substrate. Marine benthos, harmonized across two sources (Arctic Traits + New Zealand Trait Database):bioturbation(Solan/Queiros functional groups),living_habit,feeding_guild. Octocoral (Gomez-Gras et al. 2024):skeletal_rigidity,colony_growth_form. Verified end-to-end (Corallina calcified-articulated, Fucus diplontic, Laminaria haplodiplontic; the bryozoan Alcyonidium gelatinosum an attached suspension-feeder with no bioturbation). - Seven new single-source
add_trait()traits, each grounded on its source’s own values before wiring:gc_content(prokaryote genome GC %, Madin et al.- and
sporulation(endospore formation, same source), extending the prokaryote block;larval_nutrition(bee larval food, EuPollTrait);colour_lightness(beetle body greyness 0-255, saproxylic beetle traits, a melanism axis);cell_surface_area(microalgae, um2, Rimet et al., alongsidecell_length/cell_width/cell_thickness/cell_biovolume);carapace_length(turtle, mm, TurtleTraits); andparental_care(fish Balon reproductive guild guarder/non-guarder/bearer, Beukhof et al. 2019). Verified end-to-end (Thermus aquaticus GC 67%, Bacillus subtilis sporulates, leatherback carapace 2.26 m, stickleback a guarder).add_trait("fruit_type")also gains AusTraits as a third plant source.
- and
- Three new
add_trait()traits, each grounded on the source’s own values before wiring:zooxanthellate(coral symbiotic state, from the Coral Trait Database and the octocoral dataset, which share the zooxanthellate/azooxanthellate vocabulary),optimal_growth_ph(prokaryote, from Madin et al. 2020, the pH companion tooptimal_growth_temperature), andcell_thickness(microalgae, from Rimet et al., alongside the existingcell_length/cell_width/cell_biovolume). -
add_trait("diet_guild")now also draws on EltonTraits (Wilman et al. 2014), extending diet guilds from birds and reptiles to mammals. The guild is derived from the ten EltonTraits diet fractions (built intoelton_traits.vtr) and agrees 93% with EltonTraits’ own diet classification and 83% with AVONET.add_elton_traits()exposes the samediet_guildcolumn directly. -
add_trait("ellenberg_salt")gains Baseflor as a third source: its salinity column is on the same 0-9 Ellenberg scale as FloraWeb and Ecoflora (Pearson r = 0.88 on shared species), so it joins them without rescaling. - New
add_trait()traitsegg_lengthandegg_width(mm) gather reptile, bird, and turtle egg dimensions from the Amniote database, ReptTraits, and TurtleTraits, which agree to within measurement noise on shared species. - New
add_trait("brain_mass")(g) coalesces COMBINE with AnimalTraits (kg converted to grams), calibrated 1:1 on shared species. - Wider taxonomic coverage for several life-history traits, each source shared-species calibrated before it was added:
reproductive_frequencynow spans mammals through reptiles and turtles (AnAge, PanTHERIA, ReptTraits, TurtleTraits added);clutch_litter_sizegains turtles (TurtleTraits) and birds (Birdbase);age_at_maturitygains turtles and fish (TurtleTraits, Beukhof); andlongevitygains fish (Beukhof, whose ~390 yr maximum is the Greenland shark). -
add_trait("pollination_vector")gains FloraWeb and AusTraits, which agree 82-91% with the existing Baseflor and Ecoflora sources on shared species. Named insect taxa (bee, beetle, fly, …) resolve toinsect; AusTraits’ coarsebiotic/abioticand animal (bird, bat) records have no single vector in this vocabulary and stayNArather than being guessed. - Six new
add_trait()traits, each numeric source shared-species calibrated (ratio ~1.00) against a co-registered source before it was added:male_maturity(yr; AnAge, Amniote, COMBINE – the male analogue ofage_at_maturity),incubation_period(days; Amniote, TurtleTraits egg incubation, kept separate from the combinedgestation_incubation),diet_breadth(count of dietary categories; COMBINE, PanTHERIA, Birdbase),tongue_length(mm; Ostwald, EuPollTrait bee proboscis),aspect_ratio(caudal-fin aspect ratio; Beukhof, Quimbayo), andforaging_mode(active/ambush/mixed; ReptTraits, TurtleTraits). -
add_trait("diet_guild")gains TurtleTraits and Blanchard ant diets, mapped to the guild vocabulary by ordered regex (compound “omnivorous to carnivorous” resolves to its primary guild; ant “predator” to carnivore). - Six more
add_trait()traits:reproductive_mode(oviparous/ovoviviparous/ viviparous; ReptTraits plus Sharkipedia, whose shark strategies collapse to viviparous); the coral-habitat traitscoloniality,wave_exposure, andwater_clarity(Coral Trait DB and octocoral, sharing one vocabulary); andhead_length/head_width(mm; amphibian and beetle morphometrics). The depth traitsdepth_min/depth_maxgain coral occurrence-depth limits (Coral Trait DB, octocoral) in the same metres unit as the fish sources. - Eleven more
add_trait()traits, including a new prokaryote domain from Madin et al. (2020):gram_stain,oxygen_metabolism,cell_shape,optimal_growth_temperature(deg C), andgenome_size(bp). Alsocaudal_fin_shape(Beukhof + Quimbayo fish fin shape),voltinism(generations per year; Arthropod Traits + EuPollTrait),migration(AVONET),flightless(BIRDBASE),venomous(ReptTraits), andsociality(EuPollTrait bees).thermal_maxgains Pottier amphibian CTmax on the same degrees-C scale. - Nine more
add_trait()traits:wingspan(mm; LepTraits butterflies, whose source column is labelled mm but is actually cm, corrected on the known wingspans of monarch, cabbage white, and swallowtail);leaf_lengthandleaf_width(mm; AusTraits);fungal_trophic_mode(FUNGuild and FungalTraits);feeding_modeandmouth_position(fish; Beukhof, Quimbayo);air_breathing(FishBase);motility(prokaryote; Madin); andlecty(bee pollen host breadth; EuPollTrait).
Bug fixes
- Static enrichments now refresh their local cache when the source data is rebuilt and re-released under the same tag. Previously a version-locked enrichment never re-checked, so a cache downloaded before a rebuild stayed stale forever (silent all-NA for any newly added columns). The manifest now carries a
content_id(an md5 of the built.vtr); a static cache is reconciled against it entirely offline, re-downloading only when the bytes actually changed. A pre-existing cache with no stored id is hashed in place, so an unchanged asset is adopted without a download. Fixes staleadd_nesttrait()nest-modality columns andadd_disperse()bin-midpoint columns for anyone who cached those before the rebuild. Every enrichment asset now carries a content id, and the same gate is extended to backbones (a content id catches a same-tag republish that a version bump would miss). The read-only example database and its offline fixtures are never touched. -
add_trait()now joins genus-keyed sources ongenus: the ant-diet contribution todiet_guild(Blanchard) was silently all-NA, andfungal_trophic_mode’s FungalTraits source likewise. A registry guard test now asserts every genus-keyed source is joined correctly.
taxify 0.3.2
New features
- New
add_trait()attaches a single trait across every source that carries it, reconciling their vocabularies and units. Where eachadd_*()door joins one dataset,add_trait("seed_mass")gathers the sources: it pulls seed mass from Diaz et al. and GIFT and returns both in one unit (mg), woodiness from Zanne and GIFT in one vocabulary, and likewise plant height and SLA. The defaultmode = "coalesce"adds one value per row with the columns that document it –seed_mass,seed_mass_unit,seed_mass_sources, andseed_mass_n– which keeps chained calls tidy;mode = "wide"instead gives one harmonized column per source (seed_mass_diaz,seed_mass_gift) to inspect agreement and conflict. When sources measure a trait by different methods (for example maximum vs fine-root diameter), the value is not blended: the most complete source is reported and aseed_mass_cautioncolumn explains the difference.list_traits()lists the available traits andtrait_info()shows a trait’s sources, units, harmonization notes, and cautions. The registry ships dozens of traits spanning plant functional and root traits, animal body size, life history and morphology, diel activity, thermal limits, phenology, indicator values, and conservation status, each with a crosswalk grounded on the source values rather than assumed.
Renamed (source-named doors)
Enrichment doors are named after their source; the trait name is reserved for
add_trait().add_woodiness(),add_conservation_status(),add_invasive_status(), andadd_fish_traits()are renamed toadd_zanne()(Zanne et al. 2014),add_iucn()(IUCN Red List),add_griis()(GRIIS), andadd_fishmorph()(FISHMORPH; also avoids confusion withadd_fishbase()). The old names are removed, not deprecated – they never appeared in a release.New
add_gift()joins plant traits from GIFT, the Global Inventory of Floras and Traits (Weigelt et al. 2020), by accepted name. GIFT’s API exposes only the subset of its data it may redistribute (CC BY 4.0; restricted references excluded); taxifydb fetches that subset once at build time and it ships as a pre-built.vtr, soadd_gift()joins it offline like every other enrichment – no runtime API calls. Choose which traits to attach viacols=(anygift_column names, or"all"); the default is a convenient set of well-populated ones (woodiness, growth form, life cycle and form, habit flags, height, photosynthetic pathway, seed mass, dispersal, flowering months, deciduousness, SLA).gift_traits()browses the available columns.
taxify 0.3.1
New features
- New
reptiledbbackend: the Reptile Database (Uetz et al.), the global taxonomic reference for reptiles (snakes, lizards, amphisbaenians, turtles, crocodiles and the tuatara). It carries ~12.6k accepted species plus ~34k synonyms with full genus/family classification, filling the one vertebrate class the other backbones cover only partially. Use it like any backbone:taxify(x, backend = "reptiledb"). License: CC-BY 4.0. - New
add_repttraits()joins species-level reptile traits from ReptTraits (Oskyrko et al. 2024) by accepted name. Beyond body-size and life-history traits, it carries a per-species distribution signal – biogeographic realm, elevation range and mean climate – across all reptiles. This replaces the earlieradd_lizard_traits(), which drew on the same ReptTraits source but was mislabelled (it covered all reptiles, not lizards) and exposed only the morphology columns. License: CC-BY 4.0. - New
inspect()flags probable typos and other anomalies in a name list and returns only the anomalous rows, each labelled with what stands out and, where known, the name to use instead.inspect()does not match names against backbones itself – that istaxify()‘s job. On a character vector it runs the checks that need no matching:unknown(the genus is not in the genus register, the union of all 13 backbones’ genera, so no backbone recognises it – a real “probably not a name”),near_duplicate(a near-twin of a more frequent name in the same list, computed from the list alone, so it catches typos in names no backbone contains), andoutlier_group(a name whose kingdom group is a tiny minority of an otherwise coherent list – the lone animal or fungus among plants). To also surface the match-based anomalies, opt in withbackbones = TRUE(matches against every installed backbone, listed in the report header) or match yourself first and inspect the result –taxify(x) |> inspect()– which addstypo(fuzzy-corrected spelling),synonym(outdated name),ambiguous(homonym),case, and the geographic checksgeographic(a species with no WCVP record in a declaredregion/coords) andout_of_range(no region declared, yet the species’ range falls outside the list’s main TDWG continents; skipped for globally spread lists). Rows are ordered most-notable first and carry asuggestion; each gets atierdescribing what it needs, not how bad it is:unresolved(no usable name),review(a name is there but its identity is uncertain), ornote(correct, optional cleanup) – an anomaly may be intended.inspect()is read-only: it never alters the input or applies a correction. Narrow withmin_tier. The list-context labels cannot apply to a single name, soinspect()on one name warns. -
taxify()gains aregionargument for geographically constrained fuzzy matching. Pass TDWG botanical regions to restrict fuzzy candidates to species with WCVP records in those regions; exact matches are always kept.regionaccepts Level 3 codes (region = "BGM") or region names at any level, matched case- and accent-insensitively against a bundled WGSRPD crosswalk: a botanical country ("Belgium"), a Level 2 region ("Middle Europe"), or a Level 1 continent ("Europe", expanded to all its codes). The newcoordsargument takes ac(lon, lat)pair or a matrix/data.frame of coordinates and maps them to regions by point-in-polygon against the WGSRPD Level 3 boundaries (downloaded and cached on first use);regionandcoordsare unioned.coordsalso accepts ansf/sfcobject or aterraSpatVectorof points (reprojected automatically). The point-in- polygon test usesterraorsfwhen installed and falls back to a native implementation otherwise; the engine can be forced withoptions(taxify.pip_engine = "terra" | "sf" | "native"). The filter only narrows genuinely ambiguous fuzzy candidates – a candidate is dropped only when the same input name has another candidate that is in-region or has no WCVP range data – so non-plant matches are never affected and a name whose only candidate is out-of-region is still returned. The companionrangeargument selects which WCVP statuses count as in-region:"present"(default, any record),"native", or"introduced". - New
taxify_regions()lists the WGSRPD Level 3 botanical regions (codes and names) used byregionand byadd_wcvp(), with an optional search term. - Two new backbone backends:
"fishbase"(FishBase, ~36k accepted fish species) and"sealifebase"(SeaLifeBase, ~100k accepted non-fish aquatic species). Both resolve synonyms to their accepted names and carry kingdom / phylum / class / order classification. Use them like any other backbone, e.g.taxify("Gadus morhua", backend = "fishbase"). -
add_sealifebase()joins SeaLifeBase morphological and ecological traits (body length, mass, trophic level, depth range, vulnerability, habitat, commercial importance) to ataxify()result. It is the non-fish companion toadd_fishbase(). -
add_groot()joins species-level root traits from the Global Root Traits (GRooT) database: root diameter, specific root length, tissue density, N and C concentration, root mass fraction, lateral spread, mycorrhizal colonization and rooting depth (per-species means for 6,154 vascular plant species).
taxify 0.3.0
Breaking changes
-
add_qualifier_info()has been removed.taxify()now reports the qualifier natively (see New features), so a separate parsing pass is no longer needed. Replacetaxify(x) |> add_qualifier_info()withtaxify(x). Note the integerqualifier_position(a character index) is replaced by a two-value"genus"/"species"placement.
New features
- Species aggregates are now handled as a distinct concept.
taxify()gains anaggregatesargument, default"preserve": an aggregate name ("... agg.","... s.l.") matches the backbone’s aggregate taxon where one exists (for example in Euro+Med and WoRMS), otherwise it falls back to the binomial species.aggregates = "collapse"keeps the previous behaviour of stripping the marker and matching the binomial. -
taxify()output carries two new columns.qualifieris the canonical taxonomic qualifier with spelling variants folded to one token ("aggr.","agg"and"sensu lato"map to"agg."/"s.l.").qualifier_positionis"genus"for a leading prefix ("Cf. Pinus sylvestris") and"species"for an inline or trailing qualifier ("Pinus cf. sylvestris","Rubus fruticosus agg."). - Trait enrichment is aggregate-aware. Traits inherit down the taxonomic hierarchy: a species query receives an aggregate-level trait when no species-level value exists (for example EIVE indicator values keyed only at
"... aggr."), and an aggregate query reaches the aggregate-level trait. A single species’ trait is never propagated up to the aggregate. Setoptions(taxify.trait_provenance = TRUE)to add a<enrichment>_inheritedflag marking the inherited values. - Exported
normalize_aggregate_name()andis_aggregate_name()(build-time helpers) so the taxifydb build pipeline folds every backbone and enrichment aggregate marker (agg,aggr.,-agg,s.l.,sensu lato,coll., and aggregate ranks such asSPECIES AGGREGATE) to one canonicalaggr.form. The runtime and build sides therefore recognize aggregates uniformly across all backbones.
taxify 0.2.15
Bug fixes
-
summary()now counts abbreviated-genus matches (match_type == "abbrev", e.g."Q. robur"). Previously these were resolved correctly in the result but omitted from thematchedtotal and its breakdown, so the digest under-reported the number of matched names. Thematchedline now reads(exact, case-insensitive, fuzzy, abbrev), andmatch_tallycarries anabbrevcount.
Documentation
- Rewrote the “Getting started” vignette as a demo-first walkthrough: the one-call example, four canvas animations (matching pipeline, genus blocking, the WorldFlora speed benchmark, and the enrichment hub), and a worked trait-stacking analysis. Each section links to the dedicated vignette for depth.
- The enrichments vignette now documents
add_fungalroot()(genus-level mycorrhizal type), including the genus-keyed join and the type vocabulary, and stacks it into the European and global vascular-plant guidance.
taxify 0.2.14
New features
-
add_fungalroot()joins genus-level mycorrhizal type from the FungalRoot database (Soudzilovskaia et al. 2020, GBIF doi:10.15468/a7ujmj, CC BY-NC 4.0) to ataxify()result. Because mycorrhizal type is conserved at the genus level, the join is ongenus, so any species in a covered genus is annotated withmycorrhizal_type(AM,EcM,ErM,OM,NM, the dual types,Other, oruncertain),mycorrhizal_status, and the supporting record count. Plant genera only.
taxify 0.2.13
Bug fixes
- A leading genus-level
Cf.prefix (e.g."Cf. Pinus sylvestris") is now recorded as thecf.qualifier byclean_names(),clean_one(), andadd_qualifier_info(). Previously the prefix was stripped before matching but the qualifier was lost, so only inlinecf.(e.g."Pinus cf. sylvestris") was reported.add_qualifier_info()now also matches the prefix case-insensitively and reportsqualifier_position = 1.
taxify 0.2.12
CRAN release: 2026-06-30
New features
-
taxify_data_dir()can now be redirected with thetaxify.data_diroption or theTAXIFY_DATA_DIRenvironment variable, so the cache location is configurable (shared caches, scratch directories, the bundled example data). -
taxify_example_data()returns the path to a small bundled example database (a handful of species per backbone plus matching enrichment tables). Settingoptions(taxify.data_dir = taxify_example_data())lets matching and enrichment run fully offline.
Documentation
- Examples now run against the bundled example database instead of being wrapped in
\dontrun{}. Onlyadd_pignatti()(fetched live via TR8) andlist_enrichments()(reads the online manifest) remain in\donttest{}.
taxify 0.2.11
New features
-
add_floraweb()joins German-flora plant traits from FloraWeb (the live BfN portal carrying the BiolFlor data of Klotz, Kuehn & Durka 2002, plus Rothmaler morphology and Ellenberg indicator values). It bundles the full per-species trait profile – morphology, reproductive biology, the nine Ellenberg indicator values, ploidy and chromosome number, and chorological distribution (59_decolumns) – as a pre-built dataset, so it works offline.
Changes
add_ecoflora()now joins a bundled, pre-built Ecoflora dataset (18_ukcolumns: canopy height, leaf traits, life form, flowering phenology, pollination, seed weight, and British-calibrated Ellenberg values) instead of fetching live through TR8. It works offline and returns the full trait set rather than the previous five columns.add_pignatti()remains an on-demand TR8 source: its values are from a copyrighted publication and cannot be redistributed.
taxify 0.2.10
New features
-
add_ecoflora(),add_biolflor(), andadd_pignatti()join plant traits that taxify does not ship as a pre-built dataset, accessing them on demand through the suggested TR8 package on your own machine; taxify redistributes nothing. The reasons differ by source:add_ecoflora()adds British flowering months, pollen vector, life form, and leaf longevity (CC BY-NC-SA, which would allow redistribution, but ecoflora.org.uk has no bulk download, so it is fetched live per species);add_biolflor()adds Grime CSR strategy type, breeding system, pollen vector, life form, life span, and apomixis (usable with acknowledgement + citation per the BioFresh metadata statement, but no bulk copy is obtainable while the UFZ site is offline, so fetched live);add_pignatti()adds Italian Ellenberg-type indicator values, life form, and chorotype (copyrighted; read from the copy bundled in TR8, which TR8 redistributes, not taxify; works offline). Columns are region-suffixed (_uk/_de/_it) so they never collide withadd_baseflor(). TR8 is a Suggests dependency. If a live source (Ecoflora, BiolFlor) is unreachable the call errors rather than attaching silent NA.
taxify 0.2.9
New features
-
add_baseflor()joins plant traits from Baseflor (Programme Catminat, Julve 1998 ff.; ODbL 1.0 / CC BY-SA 2.0) to ataxify()result. It covers ~7,000 vascular plant taxa of France and neighbouring regions and adds flowering phenology (flower_begin_month,flower_end_month), pollination vector, dispersal mode, breeding system, flower colour, fruit type, woody growth form, and the continentality and salinity indicator-value axes absent from EIVE. The enrichment is registered in the manifest (list_enrichments()) with a pre-built.vtr; light/temperature/moisture/reaction/nutrient axes are left toadd_eive()and Raunkiaer life form toadd_leda().
taxify 0.2.8
Internal
- Added an end-to-end regression test (
tests/e2e/test-e2e-enrichment.R) for the enrichment join fixed in 0.2.5 (#1). It checks thatadd_conservation_status(),add_common_names(), andadd_woodiness()attach each value to the row’s own accepted taxon, stay invariant to batch composition and order, and land documented values on the correct species.
taxify 0.2.7
New features
- Abbreviated-genus names such as
"Q. robur"now resolve. A matching pass restricts the backbone to rows whose genus starts with the given initial and whose specific epithet matches, resolving only when that is unique. When two or more genera sharing the initial also share the epithet the abbreviation is ambiguous: the row is left unmatched withis_ambiguous = TRUEand the conflicting accepted IDs inambiguous_targets, rather than guessing a genus. A genus spelled out in full elsewhere in the same input takes precedence (the convention of abbreviating after first mention). Resolved rows carrymatch_type = "abbrev".
taxify 0.2.6
New features
- New
accepted_authorshipoutput column: the authorship of the resolved accepted name. For a synonym match,authorshipholds the synonym’s own author whileaccepted_authorshipholds the accepted name’s author, soaccepted_nameandaccepted_authorshiptogether form the accepted taxon’s full citation. Backbones that carry authorship populate it; sources without authorship (NCBI, OTT) returnNA.
Bug fixes
-
taxify()no longer errors with “replacement has length zero” for backbones whose.metasidecar records the build date asbuild_date(the current taxifydb build format) rather thandownload_date. Backbone metadata now reads both layouts and version formatting tolerates a missing date. This previously broke matching against the WoRMS and Open Tree of Life backbones.
Internal
- Declared the companion build package taxifydb in
Additional_repositories(https://gcol33.r-universe.dev), so its location is discoverable as required for a Suggests dependency outside the mainstream repositories.
taxify 0.2.5
Bug fixes
taxify()no longer errors with “incorrect number of dimensions” when the genus register is present but the backend-coverage file is not (the state on a clean install before any coverage download, and during package checks). An earlyreturn()evaluated inside atryCatch()expression returnedNULLfrom the pre-filter, which$<-then turned into a list; the out-of-scope pre-filter now resolves missing coverage to a no-op and preserves the result data frame.Replaced non-ASCII characters in roxygen documentation with ASCII equivalents so the PDF reference manual builds under LaTeX.
taxify 0.2.4
Bug fixes
Ambiguous homonym synonyms now resolve to the epithet-preserving accepted name (the homotypic basionym) instead of an arbitrary lowest-id candidate.
taxify("Pinus abies")resolves to Picea abies (not Picea polita), and the spuriousis_ambiguousflag is cleared when one candidate keeps the specific epithet (#2). Genuinely ambiguous names (no candidate, or several, preserving the epithet) are still flagged.Silenced tidyselect deprecation warnings emitted during fuzzy matching.
Internal
-
score_candidates()is exported (kept internal in the reference index) so the companiontaxifydbbuild pipeline can collapse each backbone key to the single accepted nametaxify()resolves it to. This corrects enrichment joins that previously landed trait/status values on within-genus neighbours (#1); the fix reaches users through rebuilt enrichment data.