Changes in version 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), and vegcz, the national classification of the Czech Republic (Chytrý 2007-2013, from Pladias: classes, alliances and 496 associations). Name one in taxify(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, alliance and association in place of family, genus and epithet, so a name at any rank rolls up. A Czech association reaches its EuroVegChecklist alliance through vegcz (which places it under its alliance) and then evc. - 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 (Cynosurion for Cynosurion cristati) resolves with the new match_type = "short_name" when its stems name one unit; otherwise it stays unmatched with the candidates in accepted_ids. Fuzzy matching of a syntaxon keeps its rank and the plant names it is built from, so Vaccinio-Piceion is not matched to the different alliance Vaccinio-Pinion. - The backbone registry records each backbone's nomenclature code (backbone_code(), the code column of list_backbones()), and the name cleaner, normalization key and stages a backbone runs are read from that code. Accepted-ID columns - n_ids and accepted_ids are in every taxify() result: 1 and the accepted_id for a name with one accepted ID, NA for an unmatched row. Two results now always rbind(), and x$n_ids > 1 is FALSE rather than logical(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")) replaces strict. taxify_ids() gives each ID a role: pick, same_name (another record of the same name, such as GBIF's doubtful Pinus sylvestris homonyms) or other_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 old strict = 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_name rather 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 through backbone = "gbif" and through the default chain, and the size estimate covers every key. The taxa attribute gains key_source (backbone / crosswalk / rematch) and requested (#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 and taxify_ids() carry pick_basis, what decided the pick over a name's other accepted IDs (status, priority, occurrences, validity, fuzzy, authorship, basionym, or id when only the lowest taxon ID separated them, which settles nothing) (#91). - add_wfo_info() and add_col_info() return the publication reference as name_published_in, the name add_gbif_info() uses; it reads the unified column taxifydb now writes for every backbone (gcol33/taxifydb#60). Documentation - The README and every article were rewritten. Get started is now vignette("taxify") (the old quickstart URL 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.R knits vignettes/*.Rmd.orig), so it no longer drifts from what the functions return. - citation("taxify") now reads inst/CITATION, with the CRAN DOI, and cite() takes the package's own entry from it. - scripts/benchmark-scale.R times lists of 1,000 to 100,000 names; the memory and WorldFlora benchmark scripts, which called taxify() with an argument it no longer has, run again, and all three results were measured for the articles that quote them. New features and speed - list_backbones() gains a title column with each backbone's full name. - Exact matching of 100,000 WFO names takes 14 s instead of 61 s, measured with scripts/benchmark-scale.R before and after the change: choosing the pick of each name scanned the whole candidate table once per name. Bug fixes - add_data(), the cross-backbone recovery of the add_() functions and inspect(backbones = TRUE) no longer repeat the taxify_multiple_ids warning for names they match internally (the joined table's names, a dataset's names); the warning still names the inputs of the taxify() call itself. - taxify(kingdom = ) now moves a name used in two kingdoms to its record in the requested kingdom instead of leaving it unmatched: Prunella with kingdom = "plants" matched the bird genus, dropped it, and returned nothing, although COL also accepts the plant genus. The row's pick_basis is "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 prints NA for a backbone without a version. - add_col_info(), add_wfo_info(), add_gbif_info() and add_hybrid_info() no longer register themselves as published datasets, which made cite() list an empty entry ("COL (2025). col_info.") and taxify_lock() record a dataset taxify_restore() could never find. Their columns come from a backbone the result already cites. - add_data() records the joined table under taxify_meta$user_data instead of among the published datasets, so cite() no longer lists it and taxify_restore() no longer reports it as missing. - inspect(backbones = TRUE) matches in the order of the default fallback chain, so its labels are the ones taxify() 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. Changes in version 0.6.0 (2026-09-30) Genus register kingdoms - The genus register (genus_register.vtr, which fills kingdom_group, taxon_group and life_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 read kingdom_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_kingdom column marks the 5,026 genera whose spelling accepted records place in more than one kingdom. For those, a matched row's kingdom_group is the kingdom of the record it matched, read from the backbone's kingdom column, and taxon_group / life_form are NA where 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_group to 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_status is native, naturalised, introduced, casual, cultivated, doubtful or extinct, and euromed_status_detail keeps the term as Euro+Med words it (endemic, introduced: uncertain degree of naturalisation). The citing references of each record are in euromed_status_source (resolve with cite(x$euromed_status_source, source = "euromed_distribution")), and cols = "all" adds the taxon's Euro+Med UUID as taxon_id, the taxon_id of the euromed backbone. region takes 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 is extinct (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.py in 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 = TRUE narrows the request to the key taxify() reported, and reports how many keys and records that leaves behind. It is off by default. - taxify_ids() gains a gbif_key column: the legacy GBIF taxon key(s) of each accepted ID, which are what GBIF serves occurrence records for. On the gbif backbone 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_id and accepted_id stay COL XR's. - method = "download" (default) submits one asynchronous GBIF download covering every key as a single taxonKey IN (...) predicate, so the list returns as one dataset under one citable DOI. It needs a GBIF account in GBIF_USER / GBIF_PWD / GBIF_EMAIL, and says which are missing before contacting GBIF. method = "search" sends unauthenticated searches and returns the records directly, with a taxon_key_requested column 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 = TRUE returns 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 on gbif, 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 on taxonKey alone 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 whose taxonKey is its subspecies and which carry the requested key only in speciesKey. On a 300-record sample of that request a taxonKey join 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's occ_download_queue(), which respects GBIF's limit of three concurrent downloads. Each chunk gets its own DOI and cite() reports all of them. - cite() reports a GBIF download's DOI beside the backbone citations. The download key travels from gbif_request() through gbif_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 @misc entry too. - New vignette, vignette("gbif-requests"), which also covers installing rgbif and storing GBIF credentials. Names with several accepted IDs - taxify() output replaces is_ambiguous and ambiguous_targets with n_ids and accepted_ids: how many distinct accepted taxa the backbone files the matched name under, and all of those IDs (|-joined, the one in accepted_id first). 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 and is_ambiguous = FALSE. n_ids counts 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 class taxify_multiple_ids; switch it off with options(taxify.warn_multiple_ids = FALSE). The verbs built on taxify() (reconcile(), synonyms(), upstream(), ...) do not repeat it. summary() shows the count as a multiple ids line. - 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 ID taxify() picked. It replaces taxify_candidates(), which only expanded tied homonyms. - n_ids and accepted_ids appear in taxify() output only when some name resolves to more than one accepted ID, which is also when the taxify_multiple_ids warning fires (unless switched off). Otherwise accepted_id says all there is to say and the two columns are left out. taxify_ids() and reconcile() work either way (reconcile() always carries n_ids). Results from two calls can then differ in columns: combine them with dplyr::bind_rows(), which fills the missing columns with NA, rather than base rbind(). - 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_ids and taxify_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 year or name_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.08 backbone (built 2026-09-22), which also carries GBIF's DOUBTFUL status instead of folding it into ACCEPTED. - reconcile() marks a name "ambiguous" when it has several accepted IDs and does not resolve to itself as accepted, and gains an n_ids column; a name accepted under its own spelling stays "unchanged" when a homonym record also exists. inspect()'s ambiguous label 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_dir elsewhere 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. Changes in version 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's taxify_meta entry carries n_refused plus a refused table (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, native wins over introduced and introduced over extinct, 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 to native. - 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 and pick_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 wcvp asset 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. for f., 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, while L. and L.f., or Rose and Lowe, stay apart. Over 10,000 wcvp keys (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. - enrichment_authorship_col(), the column enrich_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, or YYYY-MM / YYYY where the source dates it no finer), so list_backbones(), taxify_databases() and the cite() 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). Changes in version 0.5.5 (2026-09-18) - taxify_regions()'s two search examples ("belgium", "Europe") are wrapped in \donttest{}, after R CMD check --as-cran flagged 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 by add_wcvp() and every other grouped door) now distinguishes two reasons a name's rows were left NA (#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 takes location = in place of country =. 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 by enrichment_groups("alien_first_records"). Every row carries country_code for an explicit roll-up to countries. - dispersal_syndrome reads 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 as gravity, the class Baseflor, AusTraits and LEDA fill from barochory. - dispersal_syndrome leaves 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 now NA; a co-listed vector still wins (autochory, myrmecochory is ant). GIFT's unspecialized keeps 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_mg calibrates at 1.00 against Kew SID, GIFT and BIEN and now feeds seed_mass; ssd_g_cm3 is g/cm3 as built, so wood_density no longer divides it by 1000 and carries a caution (most records are air-dry densities, 1.14x GWDD on 37 shared species). add_leda() attaches clonal_growth_organ and floating_capacity_1week_pct in place of the empty clonal_growth and buoyancy. - add_bien() and add_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 .vtri index to the store it was built for and drops stale indexes when a .vtr is rewritten. Changes in version 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 own fuzzy_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_, so a taxify_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 rewrote accepted_name but 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, and mode = "fallback" disagreed with mode = "agreement" on the same query. - Three smaller inconsistencies in the core path (#60). clean_one(), an unreachable and drifted copy of clean_names(), is deleted and names_df is now required, so clean_names() is the single cleaner. An NA input gets match_type = "none" like "", which is what the documented vocabulary says (match_type is never NA). And summary() 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 bare accepted_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 through accepted_name, which is what the synonym promise in the documentation needs. - A pinned version downloads that version (#62). taxify_download(version = ) and taxify_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 (-/.vtr, enrichment-/.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, as cite() already did, and add_trait() sources carry their version instead of NA. 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. Grouped add_data() errors on conflicting values within a group, like the flat join, and uses the shared group fill. The emergency grouped path keeps the NA group, so add_common_names(lang = NA) returns names there too. - Empty input returns an empty result (#65). comm2sci(output = "result") with no match, and add_trait() or any add_*() 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 included lang and then dropped lang; 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 back unknown / unresolved; the genus is now taken from the cleaned name and looked up case-insensitively, as taxify() does. A mistyped region code ("GRE" for Greece, which is GRC) was kept as a region, and inspect() then flagged every plant with WCVP data as geographic. 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 under not checked. region and coords are 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 with NA; it returns NULL. id2name() matched a numeric id through as.character(), which writes 1e+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 $phylo tip labels read Quercus_robur where $tip_labels read Quercus robur; the phylo now 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 _sources, and matched base names as regular expressions in one of its two checks; one literal predicate now decides both. - children() and downstream() are one query (#69). children() is downstream() with the parent restricted to a genus or family, so the two no longer disagree: under rank = "any" / downto = "any" neither returns the parent itself. Both take kingdom =, as taxify() does, for a name used in more than one kingdom: on COL or GBIF, children("Morus") returned mulberries and gannets together, and downstream() chose the parent's rank by a vote across the homonyms. Without kingdom, a result that mixes kingdoms now warns. Both return the same columns, adding kingdom_group, and children() gains parent. - The sibling verbs forward matching arguments the same way (#70). synonyms(), upstream(), sci2comm(), comm2sci(), reconcile(), lowest_common() and class2tree() pass named arguments in ... on to taxify(), so fuzzy = FALSE, fuzzy_threshold, kingdom or region reach the matcher from each of them; synonyms(), upstream() and sci2comm() no longer set fuzzy = TRUE regardless. resolve = now means one thing: run the input through taxify() first, as in sci2comm(). comm2sci()'s argument, which instead changed the return type, is renamed output = 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() and add_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 the genus column the door joins on. - list_enrichments() reports every column an enrichment attaches (#76). The manifest's trait_cols had fallen behind 40 of the assets, missing the _min / _max / _n spread columns, FishBase's and SeaLifeBase's lw_* length-weight columns, bet's substrate and arthropod_traits' taxon_order. They are now taken from each asset's schema, and the manifest sync keeps them that way. - normalize_kingdom_group() and backbone_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 .vtr since 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 to xdelta3 unquoted, so a path like C:\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 for col). The paths are now quoted, the patch is fetched through the same route as the full asset (so a file:// manifest can serve it), and a failed patch names its reason when verbose. - install_backbones() refreshes an installed backbone the manifest has moved past (#82). It resolved each name through ensure_backbone(), which returns whatever .vtr is on disk, so an installed backbone was never compared and only a later taxify() call fetched the new release. It now runs the same comparison as the once-per-session check in taxify() (one shared refresh_backbone()), downloads where the installed build is behind, leaves a current one alone, and keeps a build pinned by taxify_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 that verbose = FALSE did not report. The manifest now records the base build's content id as delta_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 manifest content_id before 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.json records install_path ("patched" or "full"), and the ready message naming the path and any reason the patch was not used is shown regardless of verbose. - Installing a backbone or enrichment no longer leaves the replaced file's vectra indexes beside the new one. A download renamed the new .vtr into place and kept .vtr..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": true in meta.json by hand. The pin is still that meta.json flag, now written together with the build's content id and reported back; the version checks in taxify(), 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) until kind = 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 a pinned column. - 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_status column (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, with match_type = "basionym" and is_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 in ambiguous_targets: Lycopsis orientalis resolves to Anchusa arvensis subsp. orientalis rather than to the unplaced Lycopsis orientalis Steph. Changes in version 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 = and coords = now warn when the constraint could not be applied. With no range data for the declared region, region_range_sets() returned NULL and 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 on verbose. - inspect() names the checks it could not run, in the report header under not 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. Changes in version 0.5.1 - add_trait() no longer returns NA for 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. Changes in version 0.5.0 (2026-09-02) - A content id recorded by taxify_lock() now resolves back to bytes (#54). taxify_download_enrichment() and taxify_download() take a content_id=, fetch that exact build from the immutable copy published beside the rolling asset -- reading the manifest's content_url, or deriving /-.vtr when 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_versions and taxify.keep_backbone_versions set 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. Changes in version 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 only ACCEPTED, so WFO's UNCHECKED -- the status it gives a name it keeps as its own concept but has not reviewed -- scored level with a synonym, and Prunus dulcis came back as Prunus avium; status is now an ordered vocabulary (accepted, then unreviewed or provisional, then synonym, then misapplied) that also places COL's PROVISIONALLY ACCEPTED, AMBIGUOUS SYNONYM and MISAPPLIED, and falls through to the backbone's own is_synonym flag for a spelling it does not list. And the nomenclaturalStatus = "Valid" tiebreak was settling the ambiguity tier, so Rubus laciniatus returned Rubus ulmifolius with is_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, and taxify_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 into Lindl.. - New candidate_order() gives the candidate sort a single source of truth, shared with taxifydb's name-lookup build. Changes in version 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. taxifydb reinstates the marker at build time (gcol33/taxifydb#45), but a user still on an older gbif.vtr holds 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 left NA rather 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 with options(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 .vtr is 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, and add_wcvp() returned no range at all when matched through WFO. Sampling names from an enrichment's own .vtr puts 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 way accepted_name itself 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 the taxify_meta attribute (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 with options(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 returning recordid and republished under the tag it already held, so the pre-rebuild .vtr stayed in place indefinitely and add_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.08 assets and the republished gbif-2026.08 backbone. Every enrichment asset was rebuilt with the build-side reverse hop behind the recovery pass above and carries a resolved_backbones field naming the backbones it was expanded against; downloads grow with the wider key sets, 1.96x over the assets with a shipped counterpart (wcvp 69 to 141 MB, gift 20 to 40 MB). avonet and glonaf each drop a trait_cols entry naming a column their .vtr never carried, and griis drops recordid, which its upstream source stopped returning (gcol33/taxifydb#44). Changes in version 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.) Hill where WCVP, COL and Euro+Med all say (L.) Hull, so one of the commonest plants in Europe came back with no add_wcvp() range at all when matched through WFO. Authorship is now compared on a normalised key (case, punctuation, et vs &), 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.) Curtis in WFO, (L.) Rudolph in 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 left NA with 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). Changes in version 0.4.6 - Grouped enrichment joins (enrich_by_group(), behind add_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 concept taxify() 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 against accepted_authorship: the uniquely picked concept's rows are kept, or, when the ambiguity can't be resolved, every row for that name is dropped (left NA rather 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 no kingdom column 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 report kingdom_group "animalia" for a genuinely plant-only match. A new fixed_kingdom registry 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_threshold now accepts the integer raw-edit-count mode its own documentation describes. Any value greater than 1 was rejected outright, because vectra::fuzzy_join() itself only accepts a normalized distance in (0, 1] -- the mode was documented but never implemented end to end. A whole number >= 1 now runs the join at the loosest normalized bound and reconstructs the raw edit count from the reported fuzzy_dist (exactly raw_edits / max(len_a, len_b)) to filter matches. - alien_first_records is 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, so trait_cols is rewritten rather than preserved; a new alien_first_record_status column carries the occurrence status of the record each row's year was taken from. - glonaf is 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. Changes in version 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 stamps date +%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 weevil Acalles sulcatus from 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" and mode = "agreement" stage their base accepted_name the same way, so it keeps agreeing with mode = "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 to add_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, and add_sworm() the ecological group of 171 earthworms, a taxon with no coverage before. - p50 gains 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 894. 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_fixation and earthworm_ecological_group. climatic_temp_mean gains its first plant source. min_water_potential is kept apart from turgor_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_conductivity and huber_value gain a second source each. - virus_richness and the FAPROTAX function sets stay on their doors rather than entering add_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, so virus_record_count ships 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() and add_freshwater_insects_conus() join on genus, but their assets were written with the genus names in canonical_name and no genus column 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 on NULL, so "I have no coverage for this backbone" was read as "this backbone covers no genus": every unmatched name was stamped out_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, feeding elytra_length and a new elytra_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 was saproxylic, restricted to the deadwood guild. - add_trait() gains wing_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, so dimorphic (83 of 382 species) is a value of its own rather than a midpoint or a gap. It is kept separate from the bird trait flightless, which asks whether a species can fly at all. body_length, diet_guild and primary_habitat gain the same source; primary_habitat gains the vocabulary term open, the modal class for German carabids, in preference to narrowing it to grassland. - diet_guild also 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 term fungivore. 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.R so the decisions are not relitigated. The German red_list_iucn restates 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. And arthropod_traits body_size_mm is 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, serotiny the canopy seed-storage call, and soil_seed_bank the 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, plus bark_thickness. Coverage is thin (123 species for p50) 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-plant life_form for the same reason lichen_growth_form is), shoot_length and spore_diameter. - New from the coral and bird sources: larval_development_mode (spawner or brooder), substrate_attachment, linear_extension_rate, polyp_retractability, and primary_habitat (BIRDBASE's 14-term vocabulary over 13,067 species). Also self_compatibility from Tree of Sex. - sexual_system roughly doubles its plant coverage with AusTraits sex_type (87% agreement with Tree of Sex across 572 shared species) and gains bryophytes from BET. flowering_start and flowering_end are 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_guild gains BIRDBASE, listed last so it fills the birds AVONET and EltonTraits miss. - soil_seed_bank is 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 a caution: the default coalesce reports the most complete source rather than blending them, and explains itself in soil_seed_bank_caution. - add_trait() gains aggregate_trait_fallback, which the hybrids-and-aggregates vignette 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 with unused argument. The argument now threads through to enrich_simple() and defaults to the same option, so existing behaviour is unchanged. Grain-pinned sources go through enrich_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_polyp is 8 for every one of 3,606 species, because eight tentacles is the defining character of Octocorallia. Changes in version 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. Changes in version 0.4.2 - Removed taxify_download_vtr(), the deprecated alias of taxify_download(). Use taxify_download(). - Slimmed the bundled example database. The BIEN and AusTraits example .vtr fixtures were the full production tables (116k and 35k rows); they now carry only the few dozen species the offline examples and add_trait() tests use. Installed size drops from ~9.9 MB to ~3.2 MB. The offline examples and test coverage are unchanged. Changes in version 0.4.1 - add_hybrid_info() gains hybrid_parent_1_id / hybrid_parent_2_id: the backbone taxon_id of the accepted taxon each parent of a hybrid formula resolves to. An unresolved hybrid formula has no single backbone record, so the result's own taxon_id is NA; the two component ids now travel in these columns, each ready to pass straight to id2name(). Changes in version 0.4.0 (2026-07-23) Breaking: backend is renamed to backbone throughout the API (issue #24) - Two real concepts had one name. A backbone is a data source and its .vtr file; a backend is the taxify_backend S3 handle that knows how to read one. The name-string layer conflated them -- install_backbones(backends =) and backbone_path(backend) collided inside single lines. backbone now names the data source and its name string everywhere, backend names the S3 handle only, and resolve_backend() is the single crossing point between them. - This breaks existing code, with no aliases. taxify(backend = "wfo") is now taxify(backbone = "wfo"), and the same for synonyms(), children(), upstream(), downstream(), id2name(), reconcile(), comm2sci(), sci2comm(), class2tree(), lowest_common() and taxify_download(). install_backbones(backends =) becomes install_backbones(backbones =). - The backend output column is renamed to backbone in taxify(), taxify_long(), reconcile(), inspect() and taxify_register_coverage(). A lockfile's backends key becomes backbones, so a lockfile written before this release no longer loads. - Three other conflations went with it. comm2sci() and sci2comm() returned the accepted name as scientific_name; it is accepted_name, the name every other verb uses. Their query column and parse_name()'s input and upstream()'s query are all the caller's input string, so they are all input_name now. register_enrichment()'s source argument is the human-readable description, which enrich_simple() already called source_label; the key is name. - bb_path and vtr_path named the same value; it is vtr_path everywhere. - Not renamed: the backends key inside inst/manifest.json, and the backend column inside the published backend_coverage.vtr. Both are serialized schemas that taxifydb writes and taxify only reads, so renaming them is a coordinated release rather than a rename. The runtime translates at the boundary, and taxify_register_coverage() reports backbone. An example fixture was keyed at the wrong level - add_fungal_traits() joins on genus, but the bundled example database shipped a fungal_traits.vtr keyed on canonical_name with no genus column, 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.R and scripts/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 .vtr through the operating system. Opening a backbone costs 1.8 to 2.2 times its .vtr size, 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.json and taxify's own backbone registry, between HTML comment markers, by scripts/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). A manifest-docs workflow fails if a block goes stale. - Smaller: the taxify() match-type list in the README was missing hybrid_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_group and life_form that taxify() 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.vtr and backend_coverage.vtr are now built by taxifydb over 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, and taxify_download("register") fetches the published pair. - The register now lives at /genus_register/latest/ rather than /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 than NA) for an unresolved rank could make lowest_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() and id2name() defaulted to a single hardcoded backbone ("wfo" or "col"), so on a fresh install synonyms("Quercus robur") triggered a WFO download the default backbone set does not include. They now default to backend = NULL, which resolves to the highest-priority installed backbone, matching taxify(), reconcile() and comm2sci(). 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_col absent from the .vtr now errors instead of silently falling back to canonical_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_result method carries taxify_meta through subsetting, so add_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() copies license, available_groups and citation into the installed meta.json (licence is no longer NA for downloaded enrichments); ensure_enrichment() refuses to download an unbundled enrichment into the example database. - The bundled manifest now carries group_col for all seven grouped enrichments (griis, gidias, alien_first_records, wcvp, marine_distribution, common_names, glonaf), and enrichment_groups() falls back to the manifest when the installed meta.json lacks it. A grouped enrichment installed before the metadata was copied reported itself as ungrouped with no way to recover, because available_groups had a manifest fallback and group_col did not. Fuzzy matching picks the closest candidate (issue #23) - Candidate ranking had no distance term: score_candidates() ordered by status, rank, validity, epithet and then taxonID, so among several fuzzy candidates the winner was decided by ID and a farther name could beat a nearer one. "Carex flavaa" resolved to Carex flacca (distance 0.167) while Carex 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.R asserted nothing: it printed its agreement rates with cat(), so a matching regression reported a worse number and passed, and it read from a hardcoded J:/ path. It now locates the corpus through TAXIFY_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. See tests/e2e/README.md for the baseline and how to read a disagreement. - Assertions that could not fail were replaced: the trait-registry vocabulary check drove every crosswalk with NA only, test-doors.R accepted one populated column out of twenty, children() case-insensitivity held at zero rows, summary() was matched with grepl("3", .), and the multi-backend fallback test used fixtures that both carried the species. Self-fulfilling comparisons in test-default-backend.R, test-add-trait.R, test-region.R, test-gap-verbs.R and test-new-verbs.R now 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(), and taxify_restore()'s version_drift / content_drift / missing branches. 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_join errors now warn instead of silently returning no matches, and taxify() validates fuzzy and fuzzy_threshold. Internals: backend registry and shared enrichment join (issues #20, #22) - The 15 backend-*.R clones 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 one enrich_from_backbone() helper, and add_data() uses a vectorized, typed-NA join keyed on accepted_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. Suggests now names taxifydb (>= 0.1.19), the first version whose number identifies code that carries the function. Documentation (issues #18, #19) - Every \donttest example now runs offline against the example database or is \dontrun, so R CMD check --run-donttest never triggers a download. UTF-8 BOMs removed from three R files and three vignettes; taxify()'s @return documents all 26 columns; the FISHMORPH trait list matches the columns add_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 whose match_type was NA. 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 as out_of_scope without ever consulting GBIF. - The default backend = NULL path 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, so region= could not constrain a marine name COL does not list. taxify("Parvocaris samnitica") now resolves through WoRMS instead of returning out_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. - backend and backbone_version now name only the rows a backbone lookup actually resolved. They were stamped whenever match_type was non-NA, which swept in the verdicts a backbone never produced: an out_of_scope row read the leading backend's name and its resolved version even though no lookup found it there. Selecting result[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 them NA. 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 meow boundary entry: 196,807 polygon vertices across all 232 Marine Ecoregions of the World, resolved through download_backbone() the way wgsrpd is. Reference geometry, so it does not join the taxonomic backbone set that list_backbones() reports. - New marine_distribution enrichment entry: WoRMS distribution records rolled up to MEOW ecoregions, keyed on canonical_name with a region_code and a native/introduced status, the marine counterpart of the WCVP range table. - taxify_regions() lists both vocabularies region= accepts, and gains a scheme argument ("all", "wgsrpd", "meow") and a scheme column. 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 as name, province as level2_name, realm as level1_name. Marine rows appear only when the asset is installed, which is also when they resolve. Changes in version 0.3.20 Region filtering generalizes beyond plants (issue #21) - region= and coords= now constrain fuzzy matching for marine taxa, not only vascular plants. When the marine_distribution asset is installed, region_range_sets() unions two providers behind the unchanged arguments: WCVP (plants, keyed on TDWG Level 3 tdwg_code) and the marine distribution table (keyed on MEOW ecoregion region_code). Each resolved region code is routed to its provider by vocabulary -- alphabetic TDWG codes to WCVP, numeric MEOW ECO_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 new meow.vtr boundary 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_distribution enrichment 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 role rgbif::name_parse() or gnparser fill. It reuses the same cleaning pipeline taxify() 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 of downstream(). - sci2comm() resolves scientific names to their common (vernacular) names -- the forward complement of comm2sci(), reading the bundled common_names data 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 as unchanged, synonym, misspelling, ambiguous, or unresolved, 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, and taxify_restore() compares a lockfile to the current install and reports ok / 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 check warning about an unresolvable taxifydb::build_meow reference. 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. Changes in version 0.3.19 Five new name-resolution verbs - comm2sci() resolves common (vernacular) names to scientific names -- the reverse of add_common_names(). Reads the bundled common_names data offline; resolve = TRUE runs the matches through taxify() and returns an enrichable taxify_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 the taxon_id / accepted_id columns taxify() emits. - downstream() lists every accepted taxon at a chosen rank (species by default) beneath a higher taxon, where children() gives only the immediate level. - class2tree() assembles a taxonomy tree (Newick, plus an ape phylo object when ape is installed) from a set of resolved names, and lowest_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. Changes in version 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(). Changes in version 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 .meta build sidecar it leaves behind, restoring the invariant that a downloaded backbone carries only its meta.json. Previously the leftover .meta shadowed meta.json in format_backbone_version(), so a freshly downloaded release (e.g. Euro+Med 2026.07) was still labelled with the old built version (2020.1) in the backbone_version output column. meta.json and manifest reads tolerate a UTF-8 BOM - A meta.json or manifest carrying a leading UTF-8 byte-order mark (as written by Windows PowerShell's Set-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. Changes in version 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 the life_form / kingdom_group / taxon_group output columns, out-of-scope detection, and inspect()'s no-backbone checks. Previously the only builder was internal, so taxify_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_(); 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-time meta.json (the manifest version) before the backend constructor's static default, so a downloaded backbone reports the version it was downloaded at. - Manifest: the reptiledb backbone points at the reptiledb-2026.07 release (byte-identical to 2026.06) and records its content id; the placeholder register entry is removed. Changes in version 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, as add_alien_first_records() does for countries. - add_gidias() with no group is 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, so gidias_seicat_category is NA on the other groups. Requires the rebuilt gidias enrichment. - cols now 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's note field, so an access date recorded in the manifest reaches the BibTeX entry. Entries carrying no note are unchanged. Changes in version 0.3.14 A wrong temperature reached users, and is fixed - FishTraits shipped a -1 missing-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 as ft_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_tsn and ft_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_min and climatic_temp_max (degrees C): the coldest-month and warmest-month temperature at a species' range centroid, from FishTraits. They join climatic_temp_mean in the climatic-niche family, which stays apart from the organismal thermal_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 -1 code above was fixed. Plant chromosome numbers - chromosome_number reported 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 in chromosome_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") attaches cval_original_reference and cval_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_number trait continues to come from Kew alone. CCDB covers seven times more species, but a source reachable only where a build tool is installed would make add_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 prokaryote genome_size trait 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. Changes in version 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), and add_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 from thermal_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_min gain the freshwater thermal-tolerance database (CTmax and CTmin of freshwater fish, invertebrates and amphibians). - body_mass, longevity, clutch_litter_size and diet_guild gain the European bat dataset; longevity and age_at_maturity gain FishTraits; clutch_litter_size gains the zooplankton per-clutch egg count. Changes in version 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); use mode = "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 new install_backbones(), or set it via options(taxify.default_backbones = ...). The priority order is configurable with options(taxify.backbone_priority = ...). - New exported install_backbones() downloads one or more backbones for offline use (defaults to the first-run set). - Naming a backend explicitly 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 of add_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 companion combine__src column tags every cell as "reported", "imputed", or NA, keeping the measured-vs-modelled distinction explicit per cell. add_combine_reported() (measured only) and add_combine_imputed() (the imputed table on its own) are unchanged. Runtime-only; no data rebuild. Changes in version 0.3.11 Genus-resolved enrichment joins - add_cefas_btrait() now joins on genus. The Cefas benthic-traits database codes traits at genus level, so the previous species-level join returned NA for 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 new genus_fallback option on the shared enrichment engine, available to any mixed-resolution source. Changes in version 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), and add_cefas_btrait() (North-West European shelf benthic biological traits, Cefas, OGL v3.0). Each is source-prefixed (cval_, cop_, ft_, epa_, cefas_) and takes cols = "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, and eurobat are flagged static in the manifest, so their version is not re-checked each session. Changes in version 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, or DD) 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. Changes in version 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 dominant invacost_cost_type. PHYLACINE mass is not silently model-derived - add_phylacine() surfaces phylacine_mass_method and phylacine_mass_method_class, recording whether a body mass is measured (reported), allometrically estimated, or phylogenetically imputed. - add_trait("body_mass") now flags model-derived PHYLACINE masses per species: where PHYLACINE contributes an imputed or estimated mass, body_mass_caution explains it for that species only; measured masses are left unflagged. A trait registry source can attach such a per-record caution via caution_col / caution_fn, distinct from the existing whole-source method caution. Changes in version 0.3.7 Compare backbones in one call - taxify() gains a mode argument. With more than one backend, the default mode = "fallback" is the usual fallback chain, while mode = "wide" consults every backbone for every name and adds one accepted_ column plus a logical all_agree, and mode = "agreement" adds n_backbones_matched, n_distinct_accepted, and all_agree. Both are a strict superset of the standard result (a single accepted_name, still pipeable into the add_*() enrichments), so a backbone disagreement, such as GBIF keeping Macropus rufus where the Catalogue of Life resolves it to Osphranter 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 way enrichment_cols() lists a door's columns. It answers "what country / region / language codes are valid?" for add_griis(country=), add_wcvp(region=), add_common_names(lang=), and add_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 with options(taxify.aggregate_trait_fallback = FALSE), which keeps a resolved aggregate without aggregate-level data as NA. - options(taxify.trait_provenance = TRUE) now adds a character _basis column ("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 _inherited flag, so the basis of every filled value is explicit. Changes in version 0.3.6 Hybrid matching - Hybrid names are handled by type instead of being reduced to their first component. taxify() gains a hybrid_type column ("nothogenus", "nothospecies", "formula", or NA). - 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 to Salix alba; now the cross row carries match_type = "hybrid_formula" with NA backbone-match columns. The parent binomials, and their accepted names, are available on demand via add_hybrid_info(), which now adds hybrid_parent_1, hybrid_parent_2, hybrid_parent_1_accepted, and hybrid_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 returned NA against 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. Changes in version 0.3.5 Discovery - New list_backbones() lists every supported taxonomic backbone with its scope, name count, download size, version, install status, and source, mirroring list_enrichments(). New taxify_databases() stacks the backbones and enrichments into one overview so the full breadth is one call away. A canonical backbone registry now backs resolve_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. Backbones - The LCVP and WCVP backbones are downloadable as pre-built .vtr files. They previously had no published release and could only be built from source; both are now in the manifest (lcvp-3.0.1, wcvp-2026.06), so all fifteen backbones resolve without taxifydb installed. Changes in version 0.3.4 (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 the qualifier column. Now folded to their canonical tokens: the subspecies abbreviations ssp. and nssp. and the spelled-out subspecies (to subsp.); the sensu-stricto form s.s. (to s.str.); the forma spellings fo. and forma (to f.); the infraspecific ranks subvar., subf., and convar.; the notho- (hybrid) ranks nothosubsp. and nothovar. (to subsp./var., the hybrid signal being carried separately by the multiplication sign); cultivar cv.; the pathogen infrasubspecific categories f. sp. (forma specialis, previously mislabelled as a bare forma) and pv. (pathovar); the determination marker nr. ("near"); the open-nomenclature markers indet. and sp. nov.; the long and bare concept forms s. lat. and coll. (to s.l.); and the species-group markers group and gr.. Each token matches only as a whole trailing word, so it never touches a real epithet (Carex novae-zelandiae and Convallaria majalis are untouched). The qualifier registry in R/clean.R remains the single source of truth for every spelling. Changes in version 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 the family and genus already 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 an aggregate_fallback column that makes the default aggregates = "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 (TRUE on fallback, FALSE when the aggregate taxon was matched, NA for non-aggregate queries and under collapse) 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 on accepted_name across 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-source add_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 the add_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. 2020) 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., alongside cell_length/cell_width/cell_thickness/cell_biovolume); carapace_length (turtle, mm, TurtleTraits); and parental_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. - 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 to optimal_growth_temperature), and cell_thickness (microalgae, from Rimet et al., alongside the existing cell_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 into elton_traits.vtr) and agrees 93% with EltonTraits' own diet classification and 83% with AVONET. add_elton_traits() exposes the same diet_guild column 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() traits egg_length and egg_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_frequency now spans mammals through reptiles and turtles (AnAge, PanTHERIA, ReptTraits, TurtleTraits added); clutch_litter_size gains turtles (TurtleTraits) and birds (Birdbase); age_at_maturity gains turtles and fish (TurtleTraits, Beukhof); and longevity gains 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 to insect; AusTraits' coarse biotic/abiotic and animal (bird, bat) records have no single vector in this vocabulary and stay NA rather 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 of age_at_maturity), incubation_period (days; Amniote, TurtleTraits egg incubation, kept separate from the combined gestation_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), and foraging_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 traits coloniality, wave_exposure, and water_clarity (Coral Trait DB and octocoral, sharing one vocabulary); and head_length/head_width (mm; amphibian and beetle morphometrics). The depth traits depth_min/depth_max gain 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), and genome_size (bp). Also caudal_fin_shape (Beukhof + Quimbayo fish fin shape), voltinism (generations per year; Arthropod Traits + EuPollTrait), migration (AVONET), flightless (BIRDBASE), venomous (ReptTraits), and sociality (EuPollTrait bees). thermal_max gains 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_length and leaf_width (mm; AusTraits); fungal_trophic_mode (FUNGuild and FungalTraits); feeding_mode and mouth_position (fish; Beukhof, Quimbayo); air_breathing (FishBase); motility (prokaryote; Madin); and lecty (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 stale add_nesttrait() nest-modality columns and add_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 on genus: the ant-diet contribution to diet_guild (Blanchard) was silently all-NA, and fungal_trophic_mode's FungalTraits source likewise. A registry guard test now asserts every genus-keyed source is joined correctly. Changes in version 0.3.2 New features - New add_trait() attaches a single trait across every source that carries it, reconciling their vocabularies and units. Where each add_*() 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 default mode = "coalesce" adds one value per row with the columns that document it -- seed_mass, seed_mass_unit, seed_mass_sources, and seed_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 a seed_mass_caution column explains the difference. list_traits() lists the available traits and trait_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(), and add_fish_traits() are renamed to add_zanne() (Zanne et al. 2014), add_iucn() (IUCN Red List), add_griis() (GRIIS), and add_fishmorph() (FISHMORPH; also avoids confusion with add_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, so add_gift() joins it offline like every other enrichment -- no runtime API calls. Choose which traits to attach via cols= (any gift_ 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. Changes in version 0.3.1 New features - New reptiledb backend: 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 earlier add_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 is taxify()'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), and outlier_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 with backbones = TRUE (matches against every installed backbone, listed in the report header) or match yourself first and inspect the result -- taxify(x) |> inspect() -- which adds typo (fuzzy-corrected spelling), synonym (outdated name), ambiguous (homonym), case, and the geographic checks geographic (a species with no WCVP record in a declared region/coords) and out_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 a suggestion; each gets a tier describing what it needs, not how bad it is: unresolved (no usable name), review (a name is there but its identity is uncertain), or note (correct, optional cleanup) -- an anomaly may be intended. inspect() is read-only: it never alters the input or applies a correction. Narrow with min_tier. The list-context labels cannot apply to a single name, so inspect() on one name warns. - taxify() gains a region argument 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. region accepts 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 new coords argument takes a c(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); region and coords are unioned. coords also accepts an sf/sfc object or a terra SpatVector of points (reprojected automatically). The point-in- polygon test uses terra or sf when installed and falls back to a native implementation otherwise; the engine can be forced with options(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 companion range argument 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 by region and by add_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 a taxify() result. It is the non-fish companion to add_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). Changes in version 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. Replace taxify(x) |> add_qualifier_info() with taxify(x). Note the integer qualifier_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 an aggregates argument, 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. qualifier is the canonical taxonomic qualifier with spelling variants folded to one token ("aggr.", "agg" and "sensu lato" map to "agg." / "s.l."). qualifier_position is "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. Set options(taxify.trait_provenance = TRUE) to add a _inherited flag marking the inherited values. - Exported normalize_aggregate_name() and is_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 as SPECIES AGGREGATE) to one canonical aggr. form. The runtime and build sides therefore recognize aggregates uniformly across all backbones. Changes in version 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 the matched total and its breakdown, so the digest under-reported the number of matched names. The matched line now reads (exact, case-insensitive, fuzzy, abbrev), and match_tally carries an abbrev count. 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. Changes in version 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 a taxify() result. Because mycorrhizal type is conserved at the genus level, the join is on genus, so any species in a covered genus is annotated with mycorrhizal_type (AM, EcM, ErM, OM, NM, the dual types, Other, or uncertain), mycorrhizal_status, and the supporting record count. Plant genera only. Changes in version 0.2.13 Bug fixes - A leading genus-level Cf. prefix (e.g. "Cf. Pinus sylvestris") is now recorded as the cf. qualifier by clean_names(), clean_one(), and add_qualifier_info(). Previously the prefix was stripped before matching but the qualifier was lost, so only inline cf. (e.g. "Pinus cf. sylvestris") was reported. add_qualifier_info() now also matches the prefix case-insensitively and reports qualifier_position = 1. Changes in version 0.2.12 (2026-06-30) New features - taxify_data_dir() can now be redirected with the taxify.data_dir option or the TAXIFY_DATA_DIR environment 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). Setting options(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{}. Only add_pignatti() (fetched live via TR8) and list_enrichments() (reads the online manifest) remain in \donttest{}. Changes in version 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 _de columns) -- as a pre-built dataset, so it works offline. Changes - add_ecoflora() now joins a bundled, pre-built Ecoflora dataset (18 _uk columns: 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. Changes in version 0.2.10 New features - add_ecoflora(), add_biolflor(), and add_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 with add_baseflor(). TR8 is a Suggests dependency. If a live source (Ecoflora, BiolFlor) is unreachable the call errors rather than attaching silent NA. Changes in version 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 a taxify() 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 to add_eive() and Raunkiaer life form to add_leda(). Changes in version 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 that add_conservation_status(), add_common_names(), and add_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. Changes in version 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 with is_ambiguous = TRUE and the conflicting accepted IDs in ambiguous_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 carry match_type = "abbrev". Changes in version 0.2.6 New features - New accepted_authorship output column: the authorship of the resolved accepted name. For a synonym match, authorship holds the synonym's own author while accepted_authorship holds the accepted name's author, so accepted_name and accepted_authorship together form the accepted taxon's full citation. Backbones that carry authorship populate it; sources without authorship (NCBI, OTT) return NA. Bug fixes - taxify() no longer errors with "replacement has length zero" for backbones whose .meta sidecar records the build date as build_date (the current taxifydb build format) rather than download_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. Changes in version 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 early return() evaluated inside a tryCatch() expression returned NULL from 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. Changes in version 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 spurious is_ambiguous flag 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 companion taxifydb build pipeline can collapse each backbone key to the single accepted name taxify() 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.