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).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.(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.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.backbone_code(), the code column of list_backbones()), and the name
cleaner, normalization key and stages a backbone runs are read from that
code.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).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).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).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).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.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.list_backbones() gains a title column with each backbone's full name.scripts/benchmark-scale.R before and after the change: choosing the pick of each name
scanned the whole candidate table once per name.add_data(), the cross-backbone recovery of the add_<source>()
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.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.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.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.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.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).crawl_euromed_portal.py in taxifydb.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.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.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.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.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.vignette("gbif-requests"), which also covers installing
rgbif and storing GBIF credentials.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().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).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.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."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.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.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:
wcvp asset carried another name's autonym
this way (40,447 rows).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).
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.
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_<basename>, 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
(<name>-<version>/<name>.vtr, enrichment-<version>/<name>.vtr), and a
version that was never published is an error.
taxify_lock() pins the builds a result was produced from (#63). Version and
content id are recorded when an enrichment is joined, not read from whatever
is installed when the lock is written, so a refresh or restore in between no
longer changes the pin. Sources that contributed no value are left out, 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 <trait>_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 <name>.vtr.<column>.vtri, and vectra accepts an index whose
row and row-group counts match the store, whatever its content
(gcol33/vectra#13): a re-cut of the same shape would have pruned by the old
keys and dropped exact matches without an error. Every install path (backbone
and enrichment downloads, sidecar extras, builds fetched by content id)
removes the indexes of the file it replaces, and activating a stored build
drops any index the archived build left in the slot. The published assets
carry no index, so an installed asset now has the same files on every
machine.
taxify_pin() pins or releases installed backbones and enrichments by name.
A pin was reachable only by fetching a build by content id, so a project
building against a shared data directory had no call that held the builds it
had already installed, and set "pinned": 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.
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.
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.
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
<tag>/<name>-<content_id>.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.
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.
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).
(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).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.
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.
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.
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.
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().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.
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 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 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 <data_dir>/genus_register/latest/ rather than
<data_dir>/unified/latest/, so it follows the same versioned layout as
every backbone and picks up a new release through the usual version check.
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.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.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."" (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.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().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.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.[.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.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.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.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.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.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.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.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.fuzzy_join errors now warn instead of silently
returning no matches, and taxify() validates fuzzy and fuzzy_threshold.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::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.\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.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.
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.
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.
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.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.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.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.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.list_enrichments().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 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.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.
The build-from-source shims no longer pass a hardcoded version label into
taxifydb::build_<name>(); taxifydb stamps its own current version. A backbone
built from source is now labelled with the data it actually contains, not a
constant that drifts as releases advance.
format_backbone_version() falls back to the download-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.
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.
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.-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.ft_itis_tsn and ft_common_name, and produced five unusable rows named
after a family ("Percidae ."). Both are cleaned.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.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.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").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.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.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.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).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.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.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.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 = ...).install_backbones() downloads one or more backbones for offline
use (defaults to the first-run set).backend explicitly is unchanged, so existing calls keep working;
only the unset default moved off WFO.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_<trait>_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.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.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.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.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.
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.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.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_<backbone> 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.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=).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
<enrichment>_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 <enrichment>_inherited
flag, so the basis of every filled value is explicit.
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.
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.
.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.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.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.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).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).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.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.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.add_trait("brain_mass") (g) coalesces COMBINE with AnimalTraits (kg
converted to grams), calibrated 1:1 on shared species.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.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).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.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.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).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.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.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.
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.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.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".taxify_regions() lists the WGSRPD Level 3 botanical regions (codes and
names) used by region and by add_wcvp(), with an optional search term."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).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.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.")."... 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 <enrichment>_inherited
flag marking the inherited values.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.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.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.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.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.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.\dontrun{}. Only add_pignatti() (fetched live via TR8) and
list_enrichments() (reads the online manifest) remain in \donttest{}.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.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.
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.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().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."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".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.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.Additional_repositories
(https://gcol33.r-universe.dev), so its location is discoverable as required
for a Suggests dependency outside the mainstream repositories.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.
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.
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.