# countryatlas 2.0.0

A major release that wires countryatlas into the database-rendering world via
'ggsql', widens the map vocabulary, and fixes several correctness issues found
by auditing 1.0.0. The version is bumped to 2.0.0 because the bug fixes change
the output of `world_map()` (quantile binning), `bubble_map()` / `flow_map()`
(de-duplicated symbols), `geom_country_labels()` (label placement) and
`convert_country()` (override-only entities) — code that depended on the old
behaviour may see different maps or values.

## New: database-side rendering with ggsql

* `as_ggsql_source()` exports a curated, ISO-reconciled, WDI-joined table (with
  `sf` geometry WKB-encoded) as a [ggsql](https://ggsql.org) source — a DuckDB
  connection, a Parquet file, or a nanoarrow stream. countryatlas does the
  reconciliation ggsql's static bundled world can't; ggsql does the database
  push-down and Vega-Lite output countryatlas doesn't.
* `world_query()` emits a `ggsql` spatial query (`VISUALISE … DRAW spatial
  PROJECT TO … SCALE … LABEL …`) — a dependency-free string builder.
* `interactive_map(engine = "ggsql")` registers the data and renders the map in
  DuckDB, returning a Vega-Lite widget.
* `ggsql`, `duckdb`, `DBI` and `nanoarrow` are optional `Suggests`. See the new
  *countryatlas and ggsql* vignette.

## New: maps, projections and helpers

* `globe_map()` — an orthographic globe choropleth, with `backend = "sf"`
  (smoothest limb) or `backend = "polygon"` (needs only `maps` + `mapproj`).
* `spin_globe()` — a rotating-globe animated GIF (one `globe_map()` frame per
  central longitude, assembled with `gifski` or `magick`).
* `facet_map()` — small-multiple choropleths (the static counterpart to
  `animate_world()`).
* `wdj_crs()` gains eight projections (`mercator`, `winkel_tripel`, `eckert4`,
  `gall_peters`, `orthographic`, `azimuthal_equal_area`, `north_polar`,
  `south_polar`); `world_map()` / `world_geometry()` accept them all.
* `locate_country()` — point-in-polygon lookup tagging `lon`/`lat` with `iso3c`.
* `repair_country_names()` — the "act on it" companion to
  `check_country_match()`: auto-applies confident string-distance fixes.
* `country_join_all()` — reduce-join many messy country tables on the ISO spine.
* `growth_rate()`, `index_to()`, `share_of_world()` — panel analysis helpers.
* `country_overrides()` — preferred name for `wdj_overrides()` (kept as an
  alias) after the rename to countryatlas.
* `country_groups_tbl` gains `Mercosur`, `GCC`, `Nordic` and `Visegrad`.
* `country_borders()` — a tidy adjacency edge list built from polygon topology
  (`sf::st_touches()`), with `neighbors()` for a vectorised per-country lookup.
* `distance_between()` — great-circle (haversine) distance between two
  countries' centroids; needs neither `sf` nor the network.
* `dorling_map()` — the Dorling cartogram promoted to a first-class verb, with
  `k`/`itermax` tuning; `cartogram_map()` itself gains `...` passthrough to the
  underlying `cartogram::cartogram_*()` call.

## New: historical entities, inequality and spatial statistics

* `historical_codes` — a curated, dated crosswalk of dissolved entities
  (Soviet Union, Yugoslavia, Czechoslovakia, East Germany, Netherlands
  Antilles, North/South Yemen, pre-2011 Sudan, United Arab Republic,
  Tanganyika/Zanzibar, North/South Vietnam, Serbia and Montenegro) to their
  successor states, with retired ISO codes where they existed. Kosovo is
  included among the Yugoslav successors on a territory basis (documented).
* `dissolve_country()` — resolve a mixed vector of historical *and* modern
  names to successor `iso3c` rows (one-to-many, dated); modern names pass
  through as single rows, so a whole messy column pipes in unchanged.
* `check_country_match()` gains a `historical` column. It flags dissolved
  entities **even when countrycode "matches" them** — the headline case is
  `"USSR"`, which countrycode silently resolves to Russia's `RUS`, so
  Soviet-era data becomes Russian data with no warning.
* `correlate_indicators()` — pairwise indicator correlations on the spine
  (pearson/spearman, pairwise-complete, per-pair `n`), tidy long output.
* `beta_convergence()` / `sigma_convergence()` — the two standard convergence
  diagnostics: the growth-on-initial-level regression (with implied
  convergence speed and half-life) and per-year cross-country dispersion.
* `gini()` and `theil()` — inequality across countries, population-weightable;
  `theil()` decomposes exactly into between/within components when a grouping
  (continent, income) is supplied.
* `lag_by_country()` / `diff_by_country()` — panel lag and difference grouped
  by `iso3c` and ordered by `year`, completing the panel toolkit around
  `growth_rate()` / `index_to()` / `complete_years()`.
* `morans_i()` — global Moran's I with a permutation pseudo-p-value, computed
  on the row-standardised `country_borders()` adjacency. No `spdep`
  dependency: the weights come from the package's own curated topology.
* `spike_map()` — triangular spikes at country centroids (height ∝ value), the
  overplotting-resistant cousin of `bubble_map()`; needs only `maps`.
* `convert_country()` accepts `to = "name_<lang>"` (`"name_fr"`, `"name_es"`,
  `"name_zh"`, …) for localized country names via countrycode's CLDR tables.
* `world_map(style = "binned")` legends now show SI-formatted breaks
  (`4M`, not `4e+06`) when `scales` is installed; the continuous scale uses
  the same formatter.

## Bug fixes

* `bivariate_map()` errored on every call ("the condition has length > 1",
  pre-dating 2.0.0). The two fill columns were injected into
  `biscale::bi_class()` with `!!rlang::sym()`, but `bi_class()` reads them
  with `as.character(substitute(...))` rather than tidy eval, so the
  injection deparsed into a multi-element vector inside `biscale`. The
  happy path is now covered by a test (the old one only checked that the
  function errors cleanly when `sf` is *absent*).
* `as_ggsql_source()` and `interactive_map(engine = "ggsql")` errored on any
  `sf` input -- the whole point of the ggsql bridge. `sf::st_as_binary()`
  returns a classed `WKB` object, which `tibble` rejects ("all columns must
  be vectors"); the geometry column is now the plain list of raw vectors
  that `nanoarrow` encodes as binary and `DBI` writes as a `BLOB`.
* `projection = "winkel_tripel"` errored on every render -- one of the eight
  projections this release adds. The CRS built fine and the geometry
  projected fine, but `ggplot2::coord_sf()`'s graticule collapses to a
  degenerate single-point segment under PROJ's Winkel Tripel, which GEOS
  rejects ("point array must contain 0 or >1 elements"). The graticule is now
  skipped for that projection only; [theme_world_map()] blanks `panel.grid`
  anyway, so nothing visible changes. All 13 projections are now covered by a
  full-render test.
* `world_geometry("coastline", geometry = "sf")` errored with a GEOS
  `TopologyException` in every projection except `"plate_carree"`: a couple
  of Natural Earth rings are self-intersecting and `sf::st_union()` (unlike
  the spatial predicates) refuses them. The geometry is repaired before the
  union.
* `world_geometry(region = c(xmin, ymin, xmax, ymax))` -- and `world_data()`
  / `attach_geometry()` with a bounding-box `region` -- errored on the `sf`
  backend ("Loop 0 is not valid"), because `sf::st_crop()` runs under the
  strict S2 engine on unprojected geometry. It now clips with the GEOS
  planar predicate, as `country_borders()` / `locate_country()` already did.
* `convert_country()`'s `warn` argument was documented but silently ignored
  (every internal `countrycode()` call is wrapped in `suppressWarnings()`,
  because countrycode also warns on intermediate hops that
  `convert_country()` goes on to recover). It now reports inputs that match
  no country, like `standardize_country()` does. A recognised country whose
  destination value is genuinely missing still returns `NA` quietly.
* `world_map()` / `globe_map()`'s `na_label` was accepted and silently
  ignored. The `"quantile"`, `"jenks"` and `"categorical"` legends now label
  their missing-data key with it (the continuous and binned colourbars have
  no `NA` key to name, which the documentation now says).
* Kosovo's `XKX` resolves for `country` and `flag` from `from = "iso3c"`,
  not just from its name. It has no row at all in `countrycode::codelist`,
  so everything derived from the code was `NA` -- which surfaced as
  `country_borders()` / `neighbors()` returning `NA` names for Kosovo's four
  land borders, `locate_country(add = "country")` returning `NA` for points
  inside it, and `standardize_country(add = c("country", "flag"))` doing the
  same. The curated fallback table now carries the name and flag too.
* `per_capita()` without an explicit `pop` column died with an opaque
  `vctrs` error ("Can't subset columns that don't exist: `.wdj_pop`") when
  the World Bank population fetch failed or timed out -- `fetch_wdi()`
  deliberately degrades to a keys-only tibble in that case. It now reports
  the failed fetch and points at the `pop` argument.
* `theil()` returns `NA` shares (not `NaN`) for a perfectly equal
  distribution, where the total is `0` and the shares are undefined --
  matching how `gini()` and `share_of_world()` treat a zero denominator.
* `country_join()` / `country_join_all()` no longer cross-join rows whose
  `iso3c` is `NA`: unmatched countries used to collapse to a single `NA` key and
  fan out into a Cartesian product. The joins now pass `na_matches = "never"`
  (#4).
* `country_join_all()` validates the length of `origin` (must be 1 or one per
  table) instead of failing with a cryptic "missing value where TRUE/FALSE
  needed" error (#16).
* `join_world()`'s auto-detection (`detect_country_col()`) honours the candidate
  priority order instead of picking the first column by data-frame position, so
  a `region` column no longer shadows a real `country` column (#6).
* `standardize_country(add = ...)` accepts any raw `countrycode` destination
  (e.g. `"iso3n"`) again instead of erroring with "subscript out of bounds"
  (#5).
* `standardize_country(origin = "iso3c")` now validates codes: strings that are
  not real ISO 3166-1 alpha-3 codes become `NA` (and are flagged by `warn`)
  rather than passing through uppercased and unchecked (#12).
* `country_data(latest = TRUE)` / `world_data(latest = TRUE)` for a single year
  now returns each country's most recent non-`NA` value: the fetch window is
  widened so an earlier observation can actually be found (#7).
* `fetch_wdi()` keeps `iso2c` / `country` for a country that appears only in a
  non-first indicator (they are coalesced across indicators) instead of leaving
  them `NA` (#8).
* `world_map()` / `globe_map()` with `style = "quantile"` / `"jenks"` no longer
  error on a constant, single-country, or all-`NA` value column; degenerate
  breaks now fall back to a single bin (#9).
* `flow_map()` returns the base map (instead of erroring) when no
  origin-destination pair resolves to a centroid (#10).
* `aggregate_regions(fun = "min"/"max")` returns `NA` for an all-`NA` group
  instead of `Inf` / `-Inf` (#11).
* `share_of_world()` returns `NA` (not `NaN`/`Inf`) when the (per-year) total is
  zero or non-finite (#13).
* `gini()` returns `NA` with a warning for negative input rather than a value
  outside the documented `[0, 1]` range (#14).
* `spike_map()` no longer produces `NaN` spike coordinates when every height is
  zero (#15).
* `world_query()` honours `transform` even when `palette = NULL`, emitting a
  standalone `SCALE fill VIA <transform>` clause (#17).
* `world_map(style = "quantile"/"jenks")` computed breaks over polygon
  **vertices**, so a country's geometric complexity biased the quantiles and the
  bins held unequal numbers of countries. Breaks are now computed on one value
  per country.
* `bubble_map(backend = "sf")` placed bubbles in projected metres on a degrees
  base map (off the map). The base map and bubbles now share one projected CRS
  via `coord_sf()`.
* Polygon centroids returned more than one row for ten `iso3c` codes (overrides
  map several names — Azores/Madeira → PRT — to one code), fanning out joins in
  `bubble_map()` / `flow_map()`. Centroids are now one antimeridian-safe row per
  country (the largest piece).
* `geom_country_labels()` placed labels at the bounding-box midpoint over all of
  a country's pieces, so the US / Fiji / NZ labels drifted into the wrong ocean.
  Labels now sit on each country's largest piece.
* `projection = "plate_carree"` built an incoherent PROJ string
  (`+proj=longlat … +units=m`); it is now true equirectangular (`+proj=eqc`).
* `convert_country()` only applied `wdj_overrides()` for `to = "iso3c"`, so
  override-only entities (e.g. "Canary Islands", "Azores", "Bonaire") returned
  `NA` for every other destination (continent, region, iso2c, flag, currency,
  country name, ...). It now resolves the override-corrected `iso3c` first and
  derives every other destination from that.
* Kosovo's `XKX` needed extra care: it has no row at all in
  `countrycode::codelist`, so deriving destinations purely via the `iso3c`
  round-trip above is `NA` for everything — which would have *regressed*
  `flag`/`region`/`country`, since 1.0.0 already resolved those via direct
  name matching (verified against the actual 1.0.0 code). `convert_country()`
  now recovers from the original name when the `iso3c` round-trip comes back
  empty, and fills `iso2c`/`continent` (which neither path classifies) from
  the same curated fallback `standardize_country()` uses. Net effect versus
  1.0.0: zero regressions, plus newly-working `continent`/`iso2c` for Kosovo —
  which also fixes `locate_country(..., add = "continent")` for points inside
  it.
* `interactive_map(..., tooltip = )` was accepted but silently ignored by every
  engine (pre-dating 2.0.0). The `"ggiraph"` and `"leaflet"` engines now use the
  supplied `tooltip` column, defaulting to `fill` as before when omitted.
* `world_data(overrides = )` (and `attach_geometry(overrides = )`) accepted a
  custom name -> iso3c override set but silently ignored it (pre-dating 2.0.0) --
  the geometry backend always matched with the default `wdj_overrides()`. The
  override set now flows through to both the polygon and `sf` matchers, so a
  custom mapping actually changes which polygons a country claims.
* `repair_country_names()` no longer records a no-op "repair" when a dissolved
  entity's own name (e.g. "Yugoslavia", which exists in the codelist but has
  no ISO code) comes back as its closest suggestion; `dissolve_country()` is
  the right tool there and is what the report now points to.
* A mistyped column name is now reported by countryatlas rather than leaking
  out of `ggplot2` as a bare "object 'x' not found" from inside a layer, or out
  of `vctrs` as a subscript error. `world_map()`, `globe_map()`, `facet_map()`,
  `tile_map()`, `bubble_map()` (both `size` and `color`), `spike_map()`,
  `flow_map()` (`from`, `to` and `weight`), `interactive_map()` (`fill` and
  `tooltip`) and `morans_i()` all validate up front, matching the message
  `per_capita()` / `rank_countries()` / `bivariate_map()` already gave.
  `morans_i()` in particular used to blame the geometry ("not enough bordering
  countries with data") for a column that simply wasn't there.
* `audit_coverage(indicator = )` silently reported `n_missing = 0` and
  `na_rate = NaN` for a column name that isn't in `data`; it now errors.
* `world_map()` / `globe_map()` errored ("'length = 2' in coercion to
  'logical(1)'") when `na_label` was longer than one element. The first
  element is used to label the single `NA` key, and a `NULL` / `NA` label
  still leaves the default formatter alone.
* `per_capita()` sent `start = Inf` to the World Bank when `data` had a `year`
  column that was entirely `NA`; it now falls back to last year, as it already
  did for a frame with no `year` column at all. Its degraded-fetch guard also
  covers the join keys now, so a partial population fetch produces the
  actionable "pass a population column" error rather than a raw `vctrs`
  subscript error.
* `theil()` returned `NaN` when every weight was zero; it now returns `NA`,
  matching `gini()`.
* `world_map()` / `globe_map()` with `style = "categorical"` and a numeric
  `fill` column let `ggplot2` raise "Continuous value supplied to a discrete
  scale" at *build* time, naming neither the column nor the style. They now
  error at the call, name the column, and point at `"quantile"` / `"jenks"` /
  `"binned"`.
* `bivariate_map()` no longer leaks `biscale`'s "var has missing values,
  omitted in finding classes" warning, which fired on essentially every call
  because real indicators always have gaps (the classes were valid either
  way). Any other `biscale` warning still passes through.
* A `region` given as lowercase `iso3c` codes silently lost countries. Falling
  through to name matching resolved some codes by accident (countrycode's
  country-name regex is case-insensitive, so `"usa"` matched) but not others
  (`"can"` did not), so `region = c("usa", "can")` subset to the USA alone.
  Codes are now recognised in any case, matching what
  `standardize_country(origin = "iso3c")` already accepted; an all-uppercase
  unknown code is still taken at face value rather than reinterpreted as a
  country name.
* `country_codes()` silently dropped a column name it did not recognise, so a
  typo returned a table quietly missing that column; it now errors and lists
  the available shortcuts.
* Every `sf`-backed call printed three or more lines of `sf` internals to the
  console -- `"Spherical geometry (s2) switched off"`, `st_intersection`'s
  `"although coordinates are longitude/latitude ... assumes that they are
  planar"`, and the matching `"switched on"`. The source was
  `sf::st_break_antimeridian()`, which toggles the s2 engine and runs an
  intersection internally, and which sits on the path of *every* `sf` call: a
  plain `attach_geometry(geometry = "sf")` emitted them, as did `world_map()`,
  `world_geometry()`, `country_borders()`, `neighbors()`, `morans_i()`,
  `locate_country()` and `simplify_geometry()`. It is now wrapped in the same
  `quietly_sf()` helper the other `sf` calls already used, so those paths are
  silent. (The notices bypass R's condition system, so `suppressMessages()`
  could not have caught them.)
* `cartogram_map()` / `dorling_map()` never validated their `weight` or `fill`
  column, the one place the rest of the package's existence checks were missed.
  A bad `weight` reached `cartogram` as `"missing value where TRUE/FALSE
  needed"` (or, for the Dorling variant, a warning about `max()` and then a
  wrong picture), and a bad `fill` was not caught at all.
* `index_to()` likewise never checked its value column, so a typo produced a
  `dplyr` error from inside `mutate()`; `base_year` and `to` are validated too.
* Scalar arguments are validated, so a typo or an `NA` names the argument
  instead of surfacing as `"missing value where TRUE/FALSE needed"`, `classInt`'s
  `"n less than 2"`, or a `PROJ` complaint about `lat_0`. Covers `n_bins`
  (`world_map()` / `globe_map()`, on the binned path as well as the
  quantile/jenks one), `lon`/`lat`/`recenter`/`lat0` (`globe_map()` on **both**
  backends -- the polygon one goes to `coord_map()` and previously accepted a
  nonsense orientation silently), `n` (`flow_map()`), `max_height` / `width` /
  `alpha` (`spike_map()`), `max_size` / `alpha` (`bubble_map()`), `keep`
  (`simplify_geometry()`), `threshold` (`repair_country_names()`), `n_perm`
  (`morans_i()`), and `n_frames` / `fps` / `width` / `height` (`spin_globe()`).
* Two of those scalar arguments were not merely reported badly -- they drew the
  wrong thing in silence. A negative `max_height` drew `spike_map()`'s spikes
  upside down, and `globe_map(lat = )` beyond +/-90 built a CRS `PROJ` rejects,
  which only surfaced later as `coord_sf()`'s `"crs not found: is it missing?"`.
* `geom_country_labels(repel = TRUE)` silently drew plain labels when `ggrepel`
  was not installed -- the one degraded optional backend the package did not
  announce, where `classInt`, `gganimate` and `rmapshaper` all report theirs. It
  now says so once per session (the argument defaults to `TRUE`, so reporting on
  every call would be noise), and stays quiet when `repel = FALSE` was asked for.
* The number of bins no longer depends on whether `classInt` is installed. For a
  fractional `n_bins`, `classInt` truncated internally while the base-quantile
  fallback passed the fraction to `seq(length.out = )` and produced one break
  more, so the same call binned differently in different environments. `n_bins`
  is now truncated to a whole number of bins before either backend sees it.
* `simplify_geometry(keep = 0)` errored under `rmapshaper` but was silently
  accepted by the `sf::st_simplify()` fallback, so the same call behaved
  differently depending on which optional package the caller had installed. A
  proportion of zero keeps no vertices, and both paths now reject it.
* Count arguments are bounded above as well as below. The scalar checks
  required a finite number, but several call sites then coerce with
  `as.integer()`, which returns `NA` past `2^31-1` -- so `n_perm = 1e10` or
  `n_bins = 1e10` produced "NAs introduced by coercion" or, worse, "missing
  value where TRUE/FALSE needed". `n_bins`, `n` (`flow_map()`,
  `lag_by_country()`, `diff_by_country()`), `n_perm` and `n_frames` now name the
  range. `morans_i()` also dropped a `max(0L, ...)` clamp that the validation
  had made unreachable.
* `lag_by_country()` / `diff_by_country()` clamped `n <= 0` up to `1`, so a lag
  of `0` quietly returned a lag of `1`; it now errors.
* `complete_years(value = )` silently ignored a column name that wasn't in
  `data` under the default `method = "none"`, while erroring from `all_of()` for
  `"locf"` / `"linear"`; it now errors consistently.
* `interactive_map(engine = "ggsql")` now gates on `ggsql` >= 0.4.1 rather than
  mere presence. `DRAW spatial` -- the clause `world_query()` emits -- arrived
  in the ggsql *engine* at 0.4.0, while the ggsql R package is still 0.3.3,
  which accepted the call and then failed inside its own SQL front end on a
  clause it did not know. The gate now refuses with an actionable message
  instead. `?world_query` records that the clause has shipped in the engine but
  not yet in the R bindings, and that `PROJECT TO` additionally needs a spatial
  backend (for DuckDB, its `spatial` extension); `world_query()` itself remains
  a dependency-free string builder.
* `R CMD check` no longer writes to the checking user's persistent cache. The
  `\donttest{}` examples fetch from the World Bank, so the memoised on-disk
  cache was being populated under `tools::R_user_dir()` during a check; under
  check it now lives in the session temp directory instead. Normal use is
  unchanged, and `options(countryatlas.cache_dir = )` still overrides both.
  `?clear_wdi_cache` now documents where the cache lives and how to disable it.
* An unmatched country in your data could be drawn as a real country. dplyr
  joins default to `na_matches = "na"`, so an `NA` ISO code matched another
  `NA` ISO code -- and Natural Earth carries Somaliland as a polygon with no
  ISO code. Any row whose country failed to resolve therefore joined onto
  Somaliland's geometry and was plotted there; with two or more unmatched rows
  the join also fanned out many-to-many, duplicating that polygon once per
  row so the visible fill was whichever happened to be drawn last. Affected
  `attach_geometry(geometry = "sf")` (and so `join_world()` and `world_map()`
  downstream of it) and `bubble_map(backend = "sf")`. All country-keyed joins
  in the package now pass `na_matches = "never"`, which `country_join()` and
  `country_join_all()` already did; the keyless polygon is still drawn, now
  correctly as a no-data feature. A test asserts the invariant across the
  whole namespace so a new join cannot reintroduce it.
* `world_query()` emitted a silently malformed query for any argument that was
  not a single string. `sprintf()` vectorises, so `projection = c("a", "b")`
  produced two `PROJECT TO` clauses, `source = character(0)` deleted the `FROM`
  line entirely, and `title = NA` became the literal text `'NA'` -- each of
  which surfaced only later, as a parse error inside ggsql's SQL front end.
  `world_query()` and `as_ggsql_source()` now validate their string arguments
  up front and name the offending one. `NULL` still omits an optional clause,
  and an empty `title` is still allowed.
* `as_ggsql_source(format = "parquet")` built its `COPY ... TO '<path>'`
  statement by string interpolation, so a path containing an apostrophe --
  legal in a filename -- closed the SQL literal early and broke the statement.
  The path is now quoted with `DBI::dbQuoteString()`, matching the
  `dbQuoteIdentifier()` treatment the table name already had.
* `suffix = character(0)` made the whole computation vanish. `suffix` is
  `paste0()`-ed onto the value column's name, and dplyr's `"{character(0)}" :=`
  is a silent no-op -- so `growth_rate(x, g, suffix = character(0))` returned
  `x` unchanged, with no growth column and no error. `suffix = NA` produced a
  column named `gNA`, and `suffix = ""` overwrote the source column in place.
  `per_capita()`, `growth_rate()`, `index_to()`, `share_of_world()`,
  `lag_by_country()` and `diff_by_country()` now require a single non-empty
  string (`lag_by_country()`/`diff_by_country()` still accept `NULL` for the
  default suffix).
* Several arguments produced an error that named nothing rather than the
  argument at fault:
  * `convert_country(to = c("country", "continent"))` -- a plausible attempt at
    two destinations -- raised "the condition has length > 1", and a
    zero-length `to` or `from` raised "argument is of length zero".
  * `origin` did the same across every function that resolves country names.
    It is now validated once in the shared internal, so
    `standardize_country()`, `country_join()`, `country_join_all()`,
    `join_world()`, `flow_map()`, `neighbors()`, `distance_between()`,
    `in_group()`, `repair_country_names()` and `check_country_match()` are all
    covered.
  * `attach_geometry(by = character(0))` raised "argument is of length zero".
  * `aggregate_regions(by = character(0))` raised nothing at all: it grouped by
    no columns and silently collapsed the world into a single row. `by` remains
    documented as plural, so multiple grouping columns still work.
* Logical arguments are validated too, closing the same gap in two forms. The
  `borders` argument of `world_map()` and `globe_map()` fed a bare `if ()`, so
  a bad value raised one of four opaque base R errors ("missing value where
  TRUE/FALSE needed", "argument is of length zero", "the condition has length
  > 1", "argument is not interpretable as logical") -- none naming `borders`.
  Elsewhere the value went through `isTRUE()`, which never errors but silently
  turns anything that is not `TRUE` into `FALSE`, so the caller got the
  opposite of what they asked: `rank_countries(x, v, desc = "yes")` ranked
  ascending, putting the *lowest* value at rank 1, and
  `gini(x, na.rm = "yes")` kept the `NA`s and returned `NA`. All 23 logical
  arguments across the package now require `TRUE` or `FALSE` and name
  themselves when they do not get it.
* `gini()` and `theil()` returned a wrong number for a wrong-length `weights`
  vector. Both recycled it with `rep_len()`, which accepts any length
  silently, so `gini(1:10, weights = c(1, 2))` returned `0.2902` -- computed
  from an alternating 1,2 pattern -- where the correctly-weighted answer is
  `0.3`. Someone weighting by population and mistakenly passing a vector of
  the wrong length got a plausible figure and no indication anything was
  wrong. `weights` (and `theil()`'s `groups`) must now be length 1 or the
  length of `x`, and `weights` must be numeric. `complete_years()` likewise
  rejects a `years` vector that is non-numeric, empty, or contains `NA`, all
  of which it previously coerced to `NA` behind base R's warning.
* Omitting a required argument now names it. Every affected function resolved
  its column argument with `rlang::as_name()`, which raises
  `argument "x" is missing, with no default` for a missing value -- naming
  rlang's own parameter, and none of these functions has an argument called
  `x`. `world_map()`, `tile_map()`, `facet_map()`, `spike_map()`,
  `bubble_map()`, `globe_map()`, `interactive_map()`, `rank_countries()`,
  `growth_rate()`, `per_capita()`, `aggregate_regions()`, `index_to()`,
  `share_of_world()`, `lag_by_country()`, `diff_by_country()`,
  `beta_convergence()`, `sigma_convergence()`, `morans_i()`, `world_query()`,
  `country_join()`, `bivariate_map()`, `cartogram_map()` and `flow_map()` now
  report e.g. `` `fill` is required. `` Optional tidy-eval arguments are
  unaffected.
* `distance_between()` paired the wrong countries when `a` and `b` had
  mismatched lengths. It combined them through vectorised arithmetic, so R's
  recycling applied: 2 countries against 3 returned `a[1]`-`b[1]`,
  `a[2]`-`b[2]` and `a[1]`-`b[3]`, behind only base R's "longer object length
  is not a multiple" warning, and 2 against 4 recycled cleanly with no warning
  at all. Equal lengths, or a length-1 side for one-against-many, are now
  required -- the same rule `locate_country()` has always enforced for
  `lon`/`lat`.
* Three documented contracts did not match their code: `?locate_country` said
  `lon`/`lat` were "recycled together" when the function has always required
  equal lengths, and `?gini` / `?theil` promised that `weights` was "recycled
  against `x` the usual R way", which is precisely the behaviour removed
  above. All three now describe what the functions do. `locate_country()`'s
  length error also read "or an `points` sf object".
* Offline safety is now covered by tests rather than assumed. Checks run
  `\donttest{}` examples and rebuild vignettes, and CRAN policy does not allow
  either to fail for want of a network connection. `world_data()` and
  `country_data()` degrade a failed fetch to a warning and a metadata-only
  frame, and `wdi_search()` reads `WDI`'s bundled indicator list rather than
  the API; all three are now asserted, so a change that made any of them
  require a connection would break the suite.
* An unwritable cache directory silently cost you your data.
  `memoise::cache_filesystem()` does not validate the directory it is given: it
  constructs successfully and only fails when something is *written*, which
  happens deep inside the fetch. So with a read-only or otherwise unusable
  cache location, `country_data(cache = TRUE)` reported `Could not fetch
  indicator "..." from the World Bank API` and returned the country spine with
  every indicator `NA` -- blaming the API for a local permission problem, while
  the same call with `cache = FALSE` returned the data perfectly. (The
  `tryCatch()` that was meant to fall back never fired, because constructing
  the cache never errored.) The directory is now checked before use, with a
  fallback to session-only caching and a one-time message naming the real
  cause.
* Projected maps failed outright under `options(OutDec = ",")` -- the ordinary
  setting in comma-decimal locales. The PROJ strings are built by pasting
  numbers, so `recenter = 48.9` became `+lon_0=48,9`, which PROJ rejects; the
  invalid CRS then surfaced as sf's opaque "crs not found: is it missing?".
  Every number destined for a machine-readable string is now formatted with an
  explicit decimal mark.
* Two more of the same kind, triggered by `options(scipen = -10)`, which
  formats a *double* in scientific notation: `sf::st_crs(4326)` became
  `EPSG:4.326e+03` and yielded an `NA` CRS (surfacing later as
  `st_crs(x) == st_crs(y) is not TRUE` from `locate_country()`), and Natural
  Earth's scale `110` became `1.1e+02`, so `world_geometry(geometry = "sf")`
  failed with `'countries1.1e+02' is not an exported object`. Every EPSG code
  and Natural Earth scale is now an integer literal, which `scipen` does not
  affect.
* `simplify_geometry()` and `world_geometry("graticule")` are insulated from
  two upstream bugs of the same family, both reproducible without this package:
  `rmapshaper` serialises `keep` for V8, which rejects the `0,1` that
  `options(OutDec = ",")` produces, and `sf::st_graticule()` overflows the node
  stack under `options(scipen = -10)`. Both calls now run with those two
  options normalised, and the caller's settings are restored immediately
  afterwards.
* `rank_countries()` silently ranked within groups when handed a grouped frame.
  Its `mutate()` honoured the caller's `group_by()`, so the same data ranked
  `4, 1, 3, 2` ungrouped and `2, 1, 2, 1` after an incidental
  `group_by(region)` upstream in the pipe -- with `within = NULL` in both
  cases, which documents a global ranking. `rank`, `percentile` and `z_score`
  were all affected. `within` is now the only thing that sets the ranking
  scope, matching every other function here, which imposes its own grouping
  rather than inheriting the caller's. A test asserts that a grouped input
  changes no answer, and that nothing leaks grouping into its return value.
* A non-numeric value column now errors by name instead of producing nonsense.
  A factor column is easy to acquire -- `read.csv()` on a column with one stray
  non-numeric entry gives you one -- and arithmetic on it failed four different
  ways: `growth_rate()` and `per_capita()` returned a column of `NA`s behind
  base R's "'/' not meaningful for factors"; `share_of_world()`,
  `rank_countries()` and `aggregate_regions()` raised an opaque error from
  inside `dplyr::mutate()`; `gini()` managed "missing value where TRUE/FALSE
  needed"; and `morans_i()` quietly returned a plausible-looking statistic.
  These, plus `index_to()`, `diff_by_country()`, `beta_convergence()`,
  `sigma_convergence()` and `theil()`, now name the column and its actual type.
  `lag_by_country()` is deliberately unchanged: it does no arithmetic, so
  lagging a factor or character column remains legitimate.
* `aggregate_regions()` reported a figure for groups it had no data for. Values
  are dropped before aggregating, so a group whose every value is missing had
  nothing left -- and each base function got that wrong differently: `"sum"`
  returned `0`, `"mean"` and `"weighted_mean"` `NaN`, and `"min"`/`"max"`
  `-Inf`/`Inf` plus a warning. "This region's total is 0" is a claim, not an
  absence, which matters in a package built around honest missing-data
  handling. All six now return `NA`, as `"min"`/`"max"` were already meant to;
  groups that do have data are unaffected, and a partially-missing group still
  aggregates the values it has. `?aggregate_regions` documents this.
* `complete_years()` failed on a zero-row panel, where every other panel helper
  returns zero rows. With no `years` it reached
  `seq(min(numeric(0)), max(numeric(0)))` and died on base R's "'from' must be
  a finite number"; with `years` supplied it died on tidyr's "Can't recycle
  `year` (size 3) to size 0". Neither message names anything the caller did. It
  now returns the empty frame, columns intact, for all three `method` values --
  while still reporting a bad `years` or `value` argument.
* `bivariate_map()` and `cartogram_map()` (and so `dorling_map()`) failed
  inside their optional dependency when no row carried the values they need.
  This is easier to hit than it sounds: `attach_geometry()` joins
  geometry-on-the-left, so a frame with nothing in it arrives at the plotting
  verb as full-length columns of `NA`. `biscale` then indexed
  `sVar[1:(length(sVar) - 1)]`, which becomes `1:-1`, and reported "only 0's
  may be mixed with negative subscripts"; `cartogram` compared `NA` in
  `if (meanSizeError < maxSizeError)` and reported "missing value where
  TRUE/FALSE needed". Neither mentions the data. Both now say which columns are
  empty, as `spike_map()` already did, and both reject a non-numeric column by
  name. Partly-missing columns still draw from the rows that do have values.
* `gini()` could kill the R session. It computed the weighted mean absolute
  difference with `outer()`, an n-by-n matrix -- fine for the ~200 countries it
  is written for, but it is exported and accepts any numeric vector. A
  geometry-joined column is 99,338 rows, needing about 79 GB, and the process
  was killed outright: no error, no message, no result. The kernel is now the
  sorted cumulative form, O(n log n) in time and O(n) in memory, which agrees
  with the pairwise definition to floating-point noise (verified across ties,
  zero weights, single values and 340 random cases) and handles a million
  values in well under a second. Every documented figure is unchanged.
* `aggregate_regions()` silently multiplied its answer when given a frame with
  map geometry attached. The polygon backend expands each country into hundreds
  of vertex rows, so a row-wise total counts it once per vertex: for the bundled
  snapshot a regional total of 497,265 came out as 280,951,373. It is reachable
  directly off `world_data(geometry = "polygon")`. It cannot de-duplicate on
  `iso3c`, since `by = c("region", "year")` roll-ups legitimately repeat a
  country, so it now warns and says what to do instead. Country-level tables
  and panels are unaffected.
* `?audit_coverage` described the raw list it returns without mentioning that
  the object is classed and has a `print()` method, so what you actually see at
  the console is a formatted report rather than the list. Both are now
  documented.
* `world_geometry("ocean")` drew nothing at all, in every projection. Under the
  S2 engine -- sf's default since 1.0, so everywhere --
  `st_as_sfc(st_bbox(-180, -90, 180, 90))` collapses to a two-point, zero-area
  polygon, and the collapse is invisible because `st_bbox()` reports the stored
  extent instead of recomputing it from the (empty) coordinates. The rectangle
  is now built by constructing the ring explicitly, which S2 never gets to
  reinterpret, and its edges are densified so a curved projection has points
  to bend: the layer comes out at Earth's true surface area (5.1e14 m2) and
  covers 98-100% of the countries layer across all nine world projections.
  `st_break_antimeridian()` is no longer applied here at all -- with
  `lon_0 = 0` it cut the outline at +/-180, its own edges, taking it down to two
  thirds of the globe and, under Mollweide, to nothing.
* Where an ocean background cannot be drawn, `world_geometry()` now says so
  instead of returning an invisible layer: the four hemispheric projections
  (`"orthographic"`, `"azimuthal_equal_area"`, `"north_polar"`,
  `"south_polar"`) show half the globe, and a whole-globe rectangle cannot be
  recentred without covering only part of the map.
* `world_geometry("coastline")` and `world_geometry("ocean")` returned a bare
  `sfc` rather than the `sf` object `?world_geometry` promises (and that the
  other four `what` values deliver), so dplyr verbs failed on exactly those
  two. Both are now `sf`; the geometry is unchanged.
* `?world_geometry`'s `@return` was a single line naming no columns. It now
  lists what each `what` returns, and warns that the sf backend's
  `centroid_lon`/`centroid_lat` are in the object's own CRS -- projected metres,
  not degrees, so `centroid_lon` for France is `174097`, not `2.1`. Use
  `country_meta$centroid_lon` for degrees.
* `sf::st_coordinates()` failed on `world_geometry("countries", geometry =
  "sf")`, in every projection including the default. Natural Earth supplies 177
  uniform `MULTIPOLYGON`s, but `st_break_antimeridian()` runs an
  `st_intersection()` internally that collapses a single-part `MULTIPOLYGON` to
  a `POLYGON`, leaving 148 `POLYGON` + 29 `MULTIPOLYGON` -- an `sfc_GEOMETRY`
  column, which `st_coordinates()` does not support. Extracting vertices from
  the package's own geometry, an ordinary thing to want, therefore errored with
  "not implemented for objects of class sfc_GEOMETRY". The column is cast back
  to `MULTIPOLYGON`, which is a type change only: row count, codes and land
  area are unchanged. `?world_geometry` also now notes that a hemispheric
  projection returns empty geometries for the far side, where the same
  `st_coordinates()` limitation applies for a different and correct reason.
* `world_map()` accepted a frame with no geometry, returned a `ggplot` object
  without complaint, and then failed only when the plot was printed -- with
  ggplot2's "Problem while computing aesthetics ... Caused by error in
  `.data$long`", which names nothing the caller did. It validated the `fill`
  column but never the `long`/`lat`/`group` columns the polygon path needs.
  Forgetting `attach_geometry()` is the easiest mistake to make here, and it is
  easy precisely because the other plotting verbs do not need it:
  `tile_map()`, `bubble_map()`, `spike_map()` and `globe_map()` all take a
  country-level frame, so `world_map(snap, gdp)` looks like it should work too.
  It now says so at the call, and names the fix. `facet_map()` delegates to
  `world_map()` and is covered by the same check; the four country-level verbs
  are deliberately unchanged, and a test pins that asymmetry.
* `interactive_map(engine = "ggiraph")` reported a missing geometry differently
  from the other engines. It assembles its own `ggplot` rather than calling
  `world_map()`, so it bypassed that check and failed at render time on
  `.data$long`, while `engine = "plotly"` named the problem properly. Both now
  give the same message. `?interactive_map` also documents that the `"leaflet"`
  engine attaches geometry itself if handed a country-level table, which the
  others do not.
* The numeric fill styles now require a numeric column. `style = "continuous"`
  and `"binned"` reached ggplot2 and failed only when the plot was printed
  ("Discrete value supplied to a continuous scale", "Binned scales only support
  continuous data"), neither naming the column; `"quantile"` and `"jenks"` did
  not fail at all -- the break computation returns early on a non-numeric
  column, so the fill fell through to the discrete scale and drew a plausible
  map whose legend claimed quantile bins it had never computed. The reverse
  direction, `style = "categorical"` on a numeric column, was already guarded,
  so this closes the pair. `animate_world()` and `facet_map()` inherit it.
* `attach_geometry()` no longer warns when given a panel. Joining one row per
  country-year against polygon vertices is legitimately many-to-many -- it is
  what `animate_world()` and `facet_map()` are for -- so dplyr's "unexpected
  many-to-many relationship" warning was noise. The relationship is now
  declared, as the cache merge already did.
* `?repair_country_names` now says which way the `stringdist` fallback errs.
  The `threshold` argument already noted that the metric changes when
  `stringdist` is absent (Jaro-Winkler versus a length-normalised edit
  distance); it now adds that the fallback is the more conservative of the two,
  repairing a subset of what Jaro-Winkler would -- mainly missing transposed
  letters, as in "Frnace" -- and never choosing a different country. Measured
  over 120 single-typo names: 98 repaired with `stringdist`, 77 without, none
  repaired that `stringdist` did not, and no wrong repairs either way. A test
  now holds the package to that.
* `simplify_geometry()` undid the geometry-type fix above. Both simplifiers
  collapse a single-part `MULTIPOLYGON` to a `POLYGON`, so a homogeneous input
  came back as a mixed `sfc_GEOMETRY` column and `sf::st_coordinates()` failed
  on the result -- the same defect as `world_geometry("countries")`, restored
  one step downstream. The output is cast back; row count and geometry are
  unchanged.
* `simplify_geometry()`'s `keep` argument now means roughly the same thing with
  and without `rmapshaper`. The `sf::st_simplify()` fallback was given a fixed
  `dTolerance` of `(1 - keep) * 10000`, i.e. metres whatever the coordinate
  system: 9 km on a projected frame, which barely simplified anything (79% of
  vertices kept at `keep = 0.1`), and 9000 *degrees* on a lon/lat frame, which
  is meaningless and only survivable because `preserveTopology` keeps a husk.
  The tolerance is now scaled to the object's own extent, so the fallback
  behaves the same on either coordinate system and responds monotonically to
  `keep`. `?simplify_geometry` says that only `rmapshaper` honours `keep` as a
  true proportion.
* `gini()`'s negative-value warning said `Returning "NA"`, which reads as the
  two-character string rather than the missing value. It now renders as `NA`,
  matching the same correction already made elsewhere.
* The test suite no longer fails on R >= 4.6. Several tests set
  `options(scipen = -10)` to exercise the scientific-notation bugs fixed above,
  but R 4.6 clamps `scipen` to a minimum of -9 and warns ("invalid 'scipen'
  -10, used -9"), so an exact round-trip assertion failed and three warnings
  were raised -- an `ERROR` under `R CMD check` on current R, even though the
  package code itself was correct. The tests now use -9 and compare against the
  value R actually stored.
* The `wdj_overrides()` soft-deprecation notice told the wrong people. It lived
  in the shared function body, so in an interactive session it fired for
  `country_overrides()` -- the very replacement it recommends -- and for every
  public function that takes the override table as a default argument
  (`standardize_country()`, `convert_country()`, `attach_geometry()`,
  `check_country_match()`, `repair_country_names()`, `world_data()` and the
  geometry backends). Callers were advised to stop using a function they had
  never written, and the advice was unactionable. The notice now fires only for
  a direct call to `wdj_overrides()`; the table itself is unchanged. The
  documented default is now `country_overrides()`, so `?attach_geometry` and
  friends name a function the reader can actually look up.
* `?country_borders`'s whole-world example runs again. It was wrapped in
  `\dontrun{}` on the grounds that the whole-world adjacency is expensive, but
  it takes about a quarter of a second from a cold session -- only 2.7 times
  the `region = "Europe"` subset that already ran live. CRAN discourages
  `\dontrun{}` for code that can be executed, so it is now a guarded
  `\donttest{}` and is actually exercised by the check. That leaves six
  `\dontrun{}` topics, each genuinely unrunnable: a live World Bank fetch, an
  HTML widget, a DuckDB connection, a 60-frame GIF, and a call that deletes
  files.
* `?country_overrides`'s advice for accented names in a non-UTF-8 locale did
  not work in that locale. It offered de-accenting with
  `iconv(x, to = "ASCII//TRANSLIT")` as an alternative to running under UTF-8,
  but `//TRANSLIT` is itself locale-dependent: under `LC_CTYPE=C` it returns
  `NA`, or replaces each accent with `?` when given an explicit
  `from = "UTF-8"`, so nothing resolves either way. The section now says
  de-accenting has to happen while still in a UTF-8 locale, and that the ASCII
  spellings the override table carries are what work everywhere.
* A bad `options(countryatlas.workers)` reached `mclapply()`. The option is
  advertised in this file, so a stray value is reachable: `"abc"`, `NA` and
  `Inf` all became `NA` workers and surfaced as "missing value where
  TRUE/FALSE needed" from deep inside a parallel fetch, while `c(2, 4)`
  silently used the larger of the two. It is now checked, and the message names
  the option. Values below one are still clamped to one, as before -- that path
  was never the problem.
* A bad `options(countryatlas.cache_dir)` did the same thing one layer down.
  The option is documented in `?clear_wdi_cache`, and a stray value reached
  `dir.exists()`/`dir.create()`: `NA`, a number and `TRUE` each gave "invalid
  filename argument", `character(0)` gave "argument is of length zero", and a
  two-element vector gave "the condition has length > 1". It is now checked and
  the message names the option. An empty string is still accepted and still
  degrades to session-only caching; `NA_character_`, which used to degrade
  silently, now errors like the other bad values.
* An empty cache directory failed on R 4.6 but not on R 4.4. `dir.create("")`
  warns and returns `FALSE` on R 4.4, so the fallback to session-only caching
  worked; on R 4.6 it *errors* with "zero-length 'path' argument", which
  escaped the surrounding `suppressWarnings()` and propagated. An empty path is
  now recognised as "no disk cache" before the filesystem is touched, so the
  behaviour is the same on every R version.
* `?theil` now says that a row with a missing *group* is dropped along with rows
  whose value is missing, so the decomposition's `total` is computed over the
  grouped subset and can differ from the ungrouped `theil(x)`. On the bundled
  snapshot that difference is entirely Puerto Rico, which has no `region`.
  `theil()` also gains numeric anchors on the bundled data, which `gini()`
  already had.
* Every `sf`-backed verb leaked `sf`'s internal chatter as `message()`
  conditions. The console was already clean, but silencing it by redirecting the
  message *stream* leaves the conditions themselves travelling to whatever
  handler the caller installed, so `purrr::quietly()`, `testthat::expect_silent()`
  or a plain `withCallingHandlers()` around `attach_geometry()`, `neighbors()`,
  `country_borders()`, `locate_country()` or `morans_i()` still saw three to nine
  "Spherical geometry (s2) switched off" / "assumes that they are planar"
  notices. They are now muffled as well as redirected. (The comment claiming
  these notices bypass R's condition system was simply wrong -- they are
  ordinary `message()`s, and only a few GDAL diagnostics need the stream
  redirect.)
* `?world_geometry` called all four azimuthal projections "hemispheric" and said
  the far side comes back as empty geometries. That is true of `"orthographic"`
  alone; `"azimuthal_equal_area"`, `"north_polar"` and `"south_polar"` are
  Lambert equal-area and image the *whole* globe, with the far side stretched
  around the rim and nothing dropped. The page now distinguishes them and points
  at `region` for a genuine polar view, and the error `"ocean"` raises in those
  projections no longer gives "it shows one hemisphere" as the reason.
* `?world_geometry` now documents the Natural Earth features that have no ISO
  code and so come back with `iso3c` `NA` -- Somaliland at every scale, plus the
  Indian Ocean Territories and Ashmore and Cartier Islands from `"medium"` on.
* `?theme_world_map` said the theme is used by *all* the package's plotting
  functions. `bivariate_map()` is the exception -- it applies
  `biscale::bi_theme()` so the map matches biscale's own legend, and its axis
  titles, panel grid and background differ as a result. The page now names the
  exception.
* `?simplify_geometry` documented `keep` as a proportion "(0-1)", but `keep = 0`
  is rejected on both simplifier paths (it would leave nothing to draw). The
  range now reads "greater than 0 and at most 1".
* `complete_years(value = )` fabricated data in the columns it was *not* given.
  A numeric column left out of `value` was classified as a static attribute and
  carry-filled, so naming **fewer** columns invented **more** figures -- and even
  `method = "none"`, which exists to complete the grid and fill nothing, produced
  a carried-forward value for the missing year. Measure columns are now excluded
  from the attribute carry whether or not they are named; an unnamed one stays
  `NA` in the rows `complete()` adds. `value = NULL` behaves exactly as before.
* `aggregate_regions()` silently ignored `weight` for every `fun` except
  `"weighted_mean"`, returning the unweighted figure -- on European GDP per
  capita, 38,323 where the population-weighted answer is 29,896. Passing
  `weight` with any other `fun` now errors, mirroring the existing abort when
  `fun = "weighted_mean"` is given no weight.
* `flow_map()` dropped a flow whose endpoint it could not resolve without a
  word, and when nothing resolved it returned a bare world map with no arc
  layer at all. It now warns, naming the values it could not place and pointing
  at `origin` -- feeding it `iso3c` codes while `origin` still defaults to
  `"country.name"` is the usual cause, and it drew a blank map.
* `?in_group` now says that a value `origin` cannot resolve answers `FALSE`,
  indistinguishable from a country that is genuinely outside the group, and
  points at `check_country_match()` for telling the two apart.
* `world_map()` / `globe_map()` passed `palette`, `title`, `legend` and
  `na_label` to `viridisLite` and `ggplot2` unchecked, so a mistake came back in
  their vocabulary rather than the package's: `palette = c("magma", "viridis")`
  reached a bare `switch()` and reported "EXPR must be a length 1 vector", and a
  numeric `palette` was accepted without a word. A length-2 `title` or `legend`
  was accepted too, and `ggplot2` then drew both strings on top of each other.
  All four are now checked; `world_query()` already validated the two of them it
  takes (`palette` and `title`). `na_label` keeps its documented tolerance --
  there is one `NA` key, so the first element wins and a length-1 `NA` still
  means "leave the default formatter alone" -- but it now says so instead of
  doing it silently. A number is still a perfectly good *label*.
* `country_data()` / `world_data()` resolved a conflict between `latest` and the
  shape arguments silently, and with opposite precedence depending on the year:
  a multi-year `year` overrode `latest = TRUE`, while a single year had
  `latest = TRUE` override `panel = TRUE`. The winner is unchanged -- both were
  already the documented behaviour -- but the call now warns, naming the
  argument being dropped, instead of returning a shape nobody asked for.
* `locate_country(tolerance_km = )` was unvalidated, and a character value did
  not merely give an opaque message -- it produced a **wrong answer**. R compares
  `dkm <= tolerance_km` as strings when the tolerance is character, and
  `"2650" <= "a"` is `TRUE`, so every unmatched point snapped to its nearest
  country however far away it was: a mid-Pacific point came back as Fiji, where
  the documented behaviour is that open ocean stays `NA`. Now checked, before the
  `sf` gate.
* Three more scalars the validation sweep had missed now name themselves instead
  of failing in a dependency's vocabulary: `correlate_indicators(min_n = )` (an
  `NA` gave "missing value where TRUE/FALSE needed", a length-2 value "the
  condition has length > 1"), and `dorling_map(k = )` / `dorling_map(itermax = )`
  (which reported `cartogram`'s "all sizes are missing and/or non-positive" and
  an assertion naming its internal `maxiter`).
* `interactive_map(engine = "ggsql")` reported a missing `ggsql` >= 0.4.1 for a
  frame that simply had no geometry -- sending the caller after a package that
  has not shipped in the R bindings at all, only to meet the real error
  afterwards. The `sf` check now runs ahead of the package gates.
  `spin_globe()` had the same inversion for its `fill` column: its scalars were
  moved ahead of the animation gate in an earlier pass, but the column check was
  not, so a mistyped column still asked for `gifski`.
* `join_world()` could not read a column of ISO codes without being told to.
  Its fallback detector tried each character column with
  `origin = "country.name"`, which does not match most alpha-3 codes, so a
  column of them was rejected outright -- and a column *named* `iso3c` was found
  by name but still read as country names, so
  `join_world(tibble(iso3c = c("FRA", "JPN")))` warned that nothing matched and
  returned all `NA`. Detection now tries the code schemes as well and carries
  the one that worked through to the conversion, so `iso3c`, `iso_a3`, `iso2c`
  and an unrecognised name like `code` all resolve. An explicit `origin` still
  wins, and a column whose name implies a scheme is only read that way if the
  scheme actually resolves it.
* A bounding-box `region` on the **polygon** backend only filters vertices; it
  cannot clip a polygon, so a country crossing the edge keeps a truncated ring
  that `geom_polygon()` closes with a straight chord (France loses 202 of 605
  vertices and the two ends sit 15 degrees apart). Nothing in the returned
  tibble showed it, and the vignette presented the box as clipping the shapes.
  It now warns and points at `geometry = "sf"`, where the clip is a real
  `sf::st_crop()`; `?world_geometry` and the vignette say which is which.
* `attach_geometry()` on a frame that already had geometry multiplied the rows
  instead of refusing. The join is by country, and a polygon frame holds one row
  per *vertex*, so re-attaching joins a country's vertices against themselves:
  the bundled snapshot went from 99,338 rows to **310,977,360**. The call
  declares `relationship = "many-to-many"` -- correctly, since one country really
  does have many vertices -- which switches off dplyr's own guard against
  exactly this. It now errors, and says to pass the country-level table.
* `share_of_world()` on a grouped frame with no `year` column returned a share
  of the **group**, not of the world: `sum()` inside `mutate()` is per group, so
  a frame grouped by continent came back with a column that summed to 5 instead
  of 1, under a name and a help page that both say "world". A panel was already
  safe by accident, because the function regroups by `year` and that replaces the
  caller's groups. It now ignores the grouping in both cases, and says so where
  it would have mattered.
* The verbs that add a column now say when they replace one the caller already
  had. `rank_countries()` overwrote `rank`, `percentile` and `z_score`, and
  `per_capita()`, `share_of_world()`, `growth_rate()`, `index_to()`,
  `lag_by_country()` and `diff_by_country()` overwrote their target column, all
  in silence -- a user's own `rank` column simply vanished. It is a warning, not
  an error, because re-running a verb on its own output is legitimate.
  `standardize_country()` is deliberately exempt: `add` names the columns
  literally, so replacing an existing `continent` is what was asked for.
* `geom_country_labels()` on an `sf` map failed with `rlang`'s internal
  "Column `long` not found in `.data`". The layer reads the polygon backend's
  `long`/`lat` columns, and its own `aes()` was evaluated against the sf frame
  before the guard inside could run. It now errors with its own message and
  points at `ggplot2::geom_sf_text(aes(label = iso3c))`, which is documented on
  the help page too.
* `geom_country_labels()` put every country that crosses the antimeridian on the
  far side of the planet when the frame had no `group` column. `group` is what
  identifies a country's separate pieces, and the label belongs on the largest;
  without it the fallback averaged the raw longitude range, so measured against
  the largest-piece centroid Fiji was 177.8 degrees out, New Zealand 169.6, and
  even the USA 96.6 -- its Aleutian tail dragging the mid-range to the Gulf of
  Guinea. Averaging in wrapped coordinates cuts those to 0.2, 6.9 and 31.4. It
  remains an approximation, and the help page now says that placement is exact
  only while `group` is present.

## Housekeeping

* `gini()` and `theil()` returned a silent `NaN` when the input contained an
  infinity. `Inf` is not `NA`, so it passed `na.rm` and (for Theil) the
  non-positive filter, then made the mean infinite and every share `Inf/Inf`.
  Every other verb propagates an infinity visibly -- `Inf` in, `Inf` out, which
  the caller can see -- but an inequality index has no such value to report, so
  both now warn and return `NA`, as they already did for a zero total weight.
  Infinite `weights` are caught too. `NaN` is still treated as `NA` and dropped.
* `?theil` promised a tibble whenever `groups` is supplied, but every degenerate
  path -- nothing left after `na.rm`, a zero total weight, an infinity -- returns
  a single `NA` instead. The help page now says so.
* `dorling_map(k = 0)` passed validation and then failed inside `cartogram` with
  "all sizes are missing and/or non-positive". `check_number()`'s bounds are
  inclusive, so `lo = 0` admitted a value the next layer cannot use -- the same
  hole the integer ceilings were added to close, at the other end of the range.
  It is now rejected with a message naming `k`; anything above zero still works.
* `?countryatlas` gains an **Options** section. Two of the three options the
  package reads -- `countryatlas.workers` and `countryatlas.gdp_compat` -- were
  described only in this changelog, which is not reference documentation, so a
  reader of the help pages had no way to find them. (`wdj_workers()`'s own
  comment noted that the option was "advertised in NEWS", which is how a bad
  value became reachable.) All three are now documented where they are looked
  for, and a test fails if a future option is read without being listed.
* The test suite runs in a quarter of the time (`R CMD check`'s test phase went
  from 389s to 92s). One block asked `neighbors()` for each country in turn and
  then again for each of that country's neighbours, to confirm the reverse edge
  -- and `neighbors()` recomputes the whole world's `sf::st_touches()` adjacency
  on every call, so that was ~465 rebuilds and 292 seconds, four fifths of the
  suite. `neighbors()` is vectorised, so one call does the same work. The same
  properties are asserted (irreflexive, no repeated pair, every edge symmetric),
  and a failure now names the offending countries rather than only counting
  them.
* `?neighbors` now says to pass a vector rather than loop, since that cost is
  invisible from the outside: every call rebuilds the whole world's adjacency, so
  asking about one country costs the same as asking about all of them, and adding
  countries to a single call only adds the filtering. Measured here, one country
  takes about as long as 153 of them, which makes a loop over them roughly two
  orders of magnitude more work. The note names `country_borders()` as where the
  cost comes from. A test pins the fact the advice rests on -- one
  `country_borders()` call per `neighbors()` call, whatever the length of `x`.
* `?attach_geometry` claimed that Gibraltar, Hong Kong, Macao, Tuvalu and the
  British Virgin Islands have geometry in "no backend at any scale". Only
  Gibraltar does not: the other four are carried by the `sf` backend at
  `scale = "medium"`, which is the very fix the preceding sentence recommends for
  microstates. It was the *no-tile* list from `?world_tiles` -- coincidentally the
  same five names -- pasted into a paragraph about geometry. Corrected, and the
  section's coverage counts (215 snapshot countries; 210, 169 and 214 carried)
  are now pinned by a test, so an upstream Natural Earth change surfaces as a
  failure rather than as silently wrong advice.
* `as_ggsql_source(format = "parquet")` wrote into the **working directory** when
  no `path` was given: the default was the bare relative path
  `"<name>.parquet"`. CRAN policy is that a package writes nowhere but the
  session's temporary directory unless the caller says otherwise. The default is
  now a file of that name under `tempdir()`, and since the function returns the
  path the workflow is unchanged; an explicit `path` still writes exactly where
  you point it. `spin_globe()` already defaulted to `tempfile()`, and no other
  export writes at all.
* `?country_meta` and `?world_snapshot` now say that their `country` columns
  disagree, and why. `country_meta` carries `countrycode`'s English names and
  `world_snapshot` the World Bank's, so 38 of the 215 shared countries are
  labelled differently ("South Korea" against "Korea, Rep."). Each table is
  faithful to its own source and neither is wrong, but joining the two on
  `iso3c` leaves you holding two `country` columns with nothing to explain the
  difference -- the very reconciliation `country_join()` advertises, using the
  same example. The count is pinned by a test, along with the referential
  consistency of all five code columns against `country_meta`.
* `?world_data` and `?country_data` now say where the `country` label comes
  from, because the same call produces two different spellings: a successful
  fetch carries the World Bank's names ("Korea, Rep."), while the country spine
  used when the fetch returns nothing carries the `countrycode` names ("South
  Korea") -- as does every other function in the package. Nothing can reconcile
  that offline, since the World Bank spelling only exists in the response, so
  both pages now point at `iso3c` as the stable key and at
  `convert_country(iso3c, to = "country")` for one consistent set of labels.
* `utils::globalVariables()` declared 29 names where 7 are needed. Emptying it
  and reading what `R CMD check` actually reports showed the rest were covered by
  the `.data$x` idiom the code uses throughout, which needs no declaration at
  all; three of them (`subregion`, `NY.GDP.PCAP.KD`, `gdp_per_capita_2015`) never
  appeared as bare symbols anywhere, only in a comment or as string literals. A
  stale entry is worse than clutter: it silences the "no visible binding" NOTE
  for a *new* bare use of the same name, which is the warning that would
  otherwise catch a typo. A test now fails if a declared name is not a real bare
  symbol in `R/`.
* Six exported functions failed when the package was loaded but not attached --
  `countryatlas::dissolve_country()`, `distance_between()`, `country_groups()`,
  `in_group()`, `tile_map()` and `world_geometry(region = <group name>)` all died
  with "object 'historical_codes' not found" or similar. They referred to the
  bundled datasets by bare name, and a bare name resolves only while the package
  is on the search path: under `countryatlas::fn()` in a script with no
  `library()` call, the lazy-data objects are not reachable. They are now
  `countryatlas::`-qualified. Every test in the suite attaches the package, so
  nothing caught this; a static check now fails if a bare reference reappears.
* `per_capita()` failed when the caller's frame already had a column named
  `.wdj_pop`, the internal name used for the fetched population. The join
  suffixed both sides to `.wdj_pop.x` / `.wdj_pop.y`, so the column the division
  reads came back `NULL` and base R reported "replacement has 0 rows, data has
  2". Any pre-existing column of that name is now dropped before the join. Only
  the branch that fetches population was affected -- passing `pop` explicitly
  never touched it.
* Two verbs leaked someone else's message on an empty frame. `facet_map()` gave
  `ggplot2`'s "Faceting variables must have at least one value", which names
  neither the argument nor the package; it now says the frame has no rows to
  facet, and notes that the other map verbs draw an empty panel instead.
  `geom_country_labels()` ran the centroid summary over nothing, where `range()`
  warns twice and `dplyr` adds a deprecation note on top -- it now returns early,
  silent as it is on a full frame. Every other plotting verb already handled a
  zero-row frame cleanly, either drawing an empty panel or naming the reason it
  cannot.
* `?attach_geometry` now says that geometry is attached once **per row**, not
  once per country. A panel wants exactly that -- one row per country-year, each
  carrying the shape -- but a frame that repeats a country by accident draws it
  more than once, and only the last one painted is visible. dplyr's own
  many-to-many warning is suppressed by the relationship the join declares, so
  nothing signals it.
* `?country_data`'s example quoted a **retired** World Bank indicator. The bank
  replaced the `EN.ATM.CO2E.*` carbon series with the AR5 greenhouse-gas series,
  and the bundled `common_indicators` table had already been updated, but the
  example still asked for `EN.ATM.CO2E.KT` -- so anyone copying it got a warning
  and an all-`NA` column. It now uses `EN.GHG.CO2.MT.CE.AR5`, which returns data.
  `R CMD check` reports examples "OK" without failing on the warning, so nothing
  surfaced this; a test now checks every indicator code quoted in `R/`, `man/`
  or the vignettes against the bundled table.
* `country_groups_tbl` was out of date by two years in four places, while
  carrying an `as_of` stamp of 2026-06-01 that claimed otherwise. **Sweden was
  missing from NATO** (acceded 7 March 2024; Finland had been added, so the table
  had been maintained to 2023 and no further), **Angola was still in OPEC** (left
  1 January 2024), **BRICS still held only its original five** (Egypt, Ethiopia,
  Iran and the UAE joined in January 2024, Indonesia in January 2025), and **The
  Gambia was missing from the Commonwealth** (rejoined 2018). Corrected, so the
  counts are now NATO 32, OPEC 12, BRICS 10 and Commonwealth 56. Saudi Arabia is
  deliberately still absent from BRICS: it was invited in the 2024 round but has
  never confirmed accession. `in_group("Sweden", "NATO")` returned `FALSE` before
  this.
* `options(countryatlas.cache_dir = )` was ignored once a cached fetch had
  happened. The memoised fetcher was built on first use and kept for the rest of
  the session, so relocating the cache afterwards silently kept writing to the
  original directory -- and `?clear_wdi_cache` offers that option as the way to
  relocate the cache without saying it has to be set first. It only ever took
  effect because `clear_wdi_cache()` happened to reset the state. The fetcher is
  now rebuilt when the directory changes, and the "cannot write to the cache
  directory" notice is once per *directory* rather than once per session, so a
  second unwritable location is not swallowed.
* A corrupt cache entry was reported as a World Bank outage. An interrupted write
  leaves a truncated or empty `.rds`, and `readRDS()`'s "unknown input format"
  surfaced under "Could not fetch indicator ... from the World Bank API", sending
  the caller off to debug a connection that was fine -- the same misattribution
  already fixed for an unwritable cache *directory*, now fixed on the read side.
  The warning names the cache and gives the recovery command, which matters
  because the bad entry persists: every later call degrades to the country spine
  until `clear_wdi_cache(disk = TRUE)` is run. A genuine network failure still
  blames the network.
* When the cache directory was unwritable, caching stopped working altogether
  instead of falling back to the session. The in-memory memo that stands in for
  the disk cache lives in one process, but multiple indicators are fetched with
  `parallel::mclapply()`, so each worker warmed a memo and then exited with it:
  every call re-fetched every indicator, hitting the World Bank API again and
  again with nothing to show for it. Fetching is now serial when the memo is
  memory-only -- the repeated round-trips cost far more than the one-shot
  parallel speedup -- and unchanged when the disk cache is available, since a
  disk memo is shared by every worker.
* A failed indicator was dropped from the result without a word, whenever more
  than one indicator was requested. `fetch_one_safe()` degrades gracefully and
  warns -- "Could not fetch indicator ... from the World Bank API", or the
  corrupt-cache variant -- but it runs inside `parallel::mclapply()`, which
  brings back a worker's *value* and discards the conditions it signalled. Since
  having several indicators is exactly what makes the fetch fork, and
  `parallel = TRUE` is the default, the common case was the silent one: a column
  simply missing from the table with no explanation. A single indicator, which
  never forks, warned correctly -- which is why this went unnoticed. Conditions
  are now carried back and re-signalled in the calling process, on the serial
  path too so both report identically, and one problem is reported once even if
  two entries name the same series.
* Country lookups silently returned `NA` in Turkish, Azeri and Crimean Tatar
  locales. `toupper()` and `tolower()` follow `LC_CTYPE`, and in those locales
  `i` and `I` are not a case pair: `toupper("idn")` returns a *dotted* capital
  I, not `"IDN"`, and `tolower("ISO3C")` returns a *dotless* i. Five places
  folded an ASCII identifier that way and then compared it against plain ASCII,
  so every ISO code containing an `i` (IDN, IND, IRL, IRN, ISL, ISR, ITA, BIH,
  CIV, FIN, ...) failed to resolve. The failures were quiet and the blast radius
  uneven: `world_geometry(region = c("ind", "chn"))` returned Ivory Coast,
  Indonesia, Isle of Man and India -- one unfoldable element made the whole
  vector fall through to *name* matching, which then matched on substrings;
  `dissolve_country("SOUTH VIETNAM")` stopped finding its alias; and
  `join_world()` on a frame with `ISO3C` and `geo` columns picked `geo`, joining
  on the wrong column entirely. Identifier folding is now done with an explicit
  ASCII table (`ascii_upper()` / `ascii_lower()`) and does not consult the
  locale.

  This fixes countryatlas's own folding, which covers every path keyed on an ISO
  code, a column name or an alias. It cannot fix matching on a country *name*:
  that goes through `countrycode`, whose regexes are themselves locale-sensitive
  (`countrycode("Ireland", "country.name", "iso3c")` is `NA` under `tr_TR`). So
  a user with `LC_COLLATE=tr_TR` still sees name-keyed gaps -- notably the
  polygon backend, which labels its geometry by joining region *names* to codes,
  so `world_geometry(region = "IND")` comes back with 21 rows of Siachen Glacier
  instead of India. Working around that would mean forcing the C locale around
  every `countrycode` call, which risks mangling accented names for everyone
  else; it is left for upstream.
* The two-core cap that CRAN policy requires of a check was applied only when
  `_R_CHECK_LIMIT_CORES_` held the exact string `"TRUE"`. `R CMD check
  --as-cran` does set it to that -- but only when it is not already set, so the
  value that actually arrives is whatever the check flavour or CI exported, and
  R's own parser for these variables reads `"true"`, `"True"`, `"T"`, `"1"`,
  `"yes"`, `"Yes"` and `"YES"` as true as well. Under any of those spellings the
  cap did not apply and a multi-indicator fetch forked `detectCores() - 1`
  workers in the middle of a check. The test is now inverted: a value that is
  set and does not explicitly parse as false means "limit", which also covers
  `"warn"`. An explicit `"false"`, `"F"`, `"0"` or `"no"` is still honoured as a
  deliberate opt-out.
* `?world_data`'s example called `world_data(2020)` unconditionally, and the
  default `geometry = "polygon"` backend comes from the suggested `maps`
  package. `R CMD check` runs `\donttest{}` blocks, so on a check flavour
  configured without suggested packages -- CRAN runs one -- that example failed
  with "The package \"maps\" is required for the polygon geometry backend",
  taking the whole examples step down with it. Writing R Extensions requires
  code that uses a suggested package to be conditional, examples included; the
  call is now guarded with `requireNamespace("maps")`. The second call in the
  block passes `geometry = "none"` and needs nothing beyond the hard
  dependencies, so it is left to run unconditionally.
* The `beyond-the-choropleth` vignette failed to build wherever `rnaturalearth`
  was absent. Its chunk guard was `has_sf <- requireNamespace("sf")`, but the sf
  geometry backend gates on three packages -- `sf`, `rnaturalearth` and
  `rnaturalearthdata` -- so on a machine with sf but without the Natural Earth
  data the guarded chunk evaluated to `TRUE`, ran, and stopped `R CMD build`
  with "The packages \"rnaturalearth\" and \"rnaturalearthdata\" are required
  for the sf geometry backend". The other two vignettes already tested for
  `rnaturalearth`; all three now test for the same trio the code itself gates
  on. Three tests had the same incomplete guard and errored rather than
  skipping in that configuration; they now share a `skip_if_no_sf_geometry()`
  helper.
* New hex logo, drawn by the package itself (`data-raw/hex_logo.R`): an
  orthographic globe — `globe_map()`'s projection — carrying a viridis
  choropleth of `world_snapshot` GDP per capita on Natural Earth geometry
  joined by `attach_geometry()`, with `spike_map()`-style population spikes
  rising off the horizon and the binned-legend swatches under the wordmark.
* The `gdp_per_capita_2015` compatibility alias (a one-cycle deprecation shim
  from 1.0.0) is now opt-in: set `options(countryatlas.gdp_compat = TRUE)` to
  restore it. The default is `FALSE`, so `world_data()` no longer emits a
  duplicate column.
* `world_snapshot` refreshed to year **2024** (was 2022) and rebuilt with the
  latest WDI data and curated overrides.
* `country_groups_tbl` membership date bumped to 2026-06-01 (was 2024-01-01).
* `?world_snapshot` was out of sync with the rebuilt data (missing the
  "Snapshot year: 2024" note); regenerated.
* Fixed a stray orphaned code fence at the end of the *countryatlas and
  ggsql* vignette that broke its markdown structure.
* `.Rbuildignore` now excludes the session-local `.claude/` directory, which
  `git` ignores but `R CMD build` does not, so it was shipping in the tarball
  and tripping `R CMD check`'s "hidden files and directories" NOTE.
* Comments in `R/overrides.R` are ASCII-only, so no source file carries
  non-ASCII characters outside a deliberate `\U` escape.
* `?world_snapshot` no longer splits a code span across two source lines, which
  had left the checked-in `.Rd` disagreeing with what `roxygen2` regenerates.
* `beta_convergence()` failed with a bare `"subscript out of bounds"` when the
  initial levels had no spread across countries. A constant predictor makes
  `lm()` return an `NA` coefficient, which `summary()` then drops entirely, so
  the lookup for it fell off the end. It now says that the initial levels have
  no spread and why that matters.
* The forking path is now tested. `fetch_wdi(parallel = TRUE)` is the default
  for a multi-indicator request, so `wdj_lapply()`'s `mclapply` branch runs on
  one of the package's busiest code paths, yet no test reached it -- every other
  test used a single indicator or passed `parallel = FALSE`. Confirmed: a
  parallel fetch is identical to the serial one, forking preserves order, `...`
  reaches the workers, `wdj_workers()` honours `options(countryatlas.workers)`
  and CRAN's two-core limit, and an error inside a fork is surfaced rather than
  left as a `try-error` for downstream code to trip over.
* The World Bank fetch and assembly path is now tested offline. It needs the
  network, so it had no coverage at all despite holding the least obvious logic
  in the package: `fetch_wdi()`'s multi-indicator reduce-merge (shared keys are
  coalesced rather than suffixed, and values stay aligned per country-year),
  its degradation when one indicator of several fails,
  `country_data(latest = TRUE)`'s "most recent non-NA" collapse (which opens the
  window at 1960 and skips a missing latest year), the panel key, the
  duplicate-key case where two `iso2c` codes map to one `iso3c`, and that
  `cache = TRUE` really short-circuits a repeated fetch. No defects were found;
  the tests pin the behaviour.
* `spin_globe()` validates `n_frames` / `fps` / `width` / `height` / `lat`
  before gating on `gifski` / `magick`, matching how `globe_map()` orders the
  two: a bad argument is the caller's bug and the message should not depend on
  which optional packages happen to be installed.
* `tile_map()` phrased the missing-`iso3c` error differently from the three
  other verbs performing the identical check; all four now read the same.
* Half the examples that were marked `\dontrun{}` now actually run: it covered
  14 of the 55 documented topics and now covers 7. Nine verbs gained executable
  examples -- `locate_country()`, `country_borders()`, `neighbors()`,
  `morans_i()`, `simplify_geometry()`, `bivariate_map()`, `cartogram_map()`,
  `dorling_map()` and the polygon-backend `globe_map()` -- having been
  unrunnable only because their examples made a live World Bank fetch. Driven
  by the bundled `world_snapshot` instead, and guarded with
  `requireNamespace()`, they are `\donttest{}` examples that execute in under
  half a second each. The safe form of `clear_wdi_cache()` is now a live
  example too. `\dontrun{}` remains
  only where the code genuinely cannot run in a check (a network fetch, an HTML
  widget, a written GIF, `ggsql` >= 0.4.1, or deleting files).
* The tests guarding this release's fixes were verified by mutation: each fix
  was reverted in a scratch copy and the suite had to fail. 35 mutations, all
  now detected -- but four were not at first, and each pointed at a real gap:
  * `is.na()` is `TRUE` for `NaN` as well, so the checks on `theil()`'s
    zero-weight result and its shares at perfect equality passed whether the
    value was the fixed `NA` or the `NaN` the bug produced. Both now assert the
    exact value.
  * Nothing verified that quantile breaks are computed on one value per country
    rather than per polygon vertex -- the fix that forced this major version
    bump. Removing the de-duplication, or flipping the flag that controls it on
    either `world_map()` or `globe_map()`'s polygon backend, broke no test.
    There are now checks on the helper, on both call sites, and on the property
    that matters: roughly equal numbers of *countries* per colour.
  * `interactive_map(tooltip = )` was unprotected: the existing tests assert the
    returned object's class, which passes whether the argument is honoured or
    silently dropped -- exactly the pre-2.0.0 bug. Both the `"ggiraph"` and
    `"leaflet"` engines are now checked on the column they are actually handed.
* Degenerate-but-valid input is now covered by tests: perfect equality
  (`gini()` / `theil()` return `0`, not `NaN`), zero-variance columns
  (`NaN` z-scores and `NA` correlations rather than errors), duplicate
  `(iso3c, year)` panel rows, poles and antipodal great circles, collinear
  rings, all-`NA` and all-zero fill columns, and single-country frames.
* `?distance_between` and `?country_meta` now state which countries have no
  bundled centroid. `country_meta` is assembled from `countrycode::codelist`,
  which has no Kosovo row, so `distance_between("Kosovo", "Serbia")` is `NA`
  even though `neighbors("Kosovo")` and `country_borders()` know about it -- and
  ten small or dependent territories have a row but no centroid. The `@return`
  already said `NA` was possible; it did not say which countries, and the
  asymmetry with the geometry backends was surprising.
* The same double-counting affected three more places, all gated on the
  presence of a `group` column -- which polygon frames have and `sf` frames do
  not:
  * `correlate_indicators()` reported `n` and `r` over geometry rows rather than
    countries. That column exists precisely so a correlation computed on a few
    countries cannot masquerade as a world fact, so an inflated `n` defeated the
    point.
  * `audit_coverage()` reported the wrong country count and a wrong `NA` rate
    for every indicator.
  * `bubble_map(backend = "sf")` drew two bubbles for a divided country, where
    the polygon path already guaranteed one per country.
  All three now reduce whenever an `iso3c` column is present.
* Quantile and jenks breaks on the `sf` backend double-counted divided
  countries. The de-duplication added in this release skipped the `sf` path on
  the assumption that Natural Earth is one row per country, but it is not:
  Cyprus occupies two rows sharing one `iso3c` at 110m, as do Cyprus and India
  at 50m. Breaking on the raw column shifted the cut points enough to move real
  countries into the wrong bin -- Saudi Arabia and Libya changed colour in the
  bundled snapshot at `n_bins = 5`. Both backends now de-duplicate on the key,
  so "one value per country" holds exactly rather than nearly.
* `convert_country(x, to = "calling_code")` returned alpha-3 country codes
  instead of telephone calling codes -- `"FRA"` where `33` was meant. The
  shortcut was mapped to `countrycode`'s `genc3c` column, which is an ISO-style
  three-letter code, not a dialling prefix; it now maps to the `telephone`
  column, so France gives `33`, the USA `1` and Japan `81`. A source comment
  claimed the limitation was "documented as best-effort", but `?convert_country`
  never mentioned `calling_code` at all; the shortcut is now listed there.
* The `Description` field -- the text CRAN renders on the package page --
  advertised nine map idioms while the package ships eleven: `spike_map()` and
  `facet_map()` (small multiples) were missing from the vocabulary list, and the
  analysis-helper examples predated this release's inequality and convergence
  statistics. Every idiom it names now corresponds to an exported verb.
* `?countryatlas` listed `morans_i()` under "Core data assembly" while
  `_pkgdown.yml` listed it under "Analysis helpers", so the two navigational
  indexes described the same function as two different kinds of thing. It is a
  spatial statistic, so it now sits with `gini()`, `theil()` and the convergence
  measures on both surfaces.
* The `@seealso` cross-references are reciprocal. All three the package had
  pointed one way only: a reader of `?gini` was sent to `theil()` but a reader
  of `?theil` was sent nowhere, and likewise for
  `beta_convergence()`/`sigma_convergence()` and
  `dissolve_country()`/`check_country_match()`. `?theil` did not link to `gini()`
  at all -- it named Gini in prose without a cross-reference. The four related
  diagnostics (`check_country_match()`, `repair_country_names()`,
  `dissolve_country()`, plus `historical_codes`) now all reference each other.
* Every page taking a `projection` argument now says where the valid values
  are. Only `world_map()` enumerated the 13 projections and only
  `world_geometry()` pointed at it; the other eight entries said no more than
  "Projection." (`bivariate_map()`) or "Projection options for the `sf`
  backend", leaving a reader with nothing to go on. Relatedly, the three pages
  that document `projection` and `recenter` together described only the
  projection, so `recenter`'s meaning -- a central meridian -- was missing from
  `world_data()`, `join_world()` and `attach_geometry()`.
* The `rnaturalearthhires` requirement is now documented on every page that
  takes a `scale` argument, not just `?world_geometry`. Seven topics --
  `world_data()`, `join_world()`, `attach_geometry()`, `locate_country()`,
  `country_borders()`, `neighbors()` and `morans_i()` -- described `scale`
  without mentioning that `"large"` is unobtainable from CRAN, so a reader of
  any of those pages met the gate with no warning.
* `scale = "large"` was offered as a plain option but needs the
  `rnaturalearthhires` package, which is not on CRAN and is not in `Suggests`.
  Left ungated, `rnaturalearth` responded by trying to install it into the
  user's library from a non-CRAN repository and then failing obscurely. It is
  now gated with a message naming the package and the repository to get it
  from, and pointing at `scale = "medium"` (50m) as the option that needs
  nothing extra. `?world_geometry` documents the requirement, and the
  *sf & projections* vignette no longer demonstrates the scale most readers
  cannot run.
* `?country_borders` recommended a graph recipe that produced nonsense.
  `igraph::graph_from_data_frame()` treats the *first two* columns as the edge
  endpoints, and `country_borders()` returns `iso3c_a`, `country_a`, `iso3c_b`,
  `country_b` -- so columns 1 and 2 are both endpoint *A*, and passing the whole
  tibble built edges from each country's code to its own name (56 vertices
  instead of 37 for Europe, every French edge running `FRA` to `"France"`). The
  documented call now passes only the two code columns.
* `?neighbors` and `?country_borders` now warn that `igraph` also exports a
  `neighbors()` -- taking a graph and a vertex rather than country names -- so
  whichever package is attached later wins. `?country_borders` recommends
  `igraph` for turning the adjacency into a graph, which walks users straight
  into the clash, so both pages now say to qualify the call as
  `countryatlas::neighbors()`. It is the only collision between this package's
  exports and any of `dplyr`, `ggplot2`, `tidyr`, `tibble`, `sf`, `maps`, `WDI`,
  `countrycode`, `scales`, `leaflet`, `plotly`, `igraph`, `raster`, `terra`,
  `purrr`, `stringr`, `forcats`, `readr` or the base packages.
* `?country_overrides` now documents why every name in the override table is
  plain ASCII: ASCII spellings match in any locale, whereas accented spellings
  rely on `countrycode`'s own matching and resolve to `NA` under a non-UTF-8
  locale (`LC_CTYPE=C`). The note points at `iconv(x, to = "ASCII//TRANSLIT")`
  for input that may carry accents.
* The test suite is now green under CRAN's `noSuggests` configuration
  (`_R_CHECK_DEPENDS_ONLY_=true`), which runs with every optional package
  absent. Four tests called `globe_map(backend = "polygon")` or `morans_i()`
  without guarding on `mapproj` / `sf`, so they errored on the dependency gate
  instead of exercising what they were written to check.
* The quantile/jenks binning that `world_map()` and both `globe_map()` backends
  perform lived as three near-copies kept in step by hand; it is now one
  internal helper. Verified behaviour-preserving by comparing the rendered fill
  of every style x `n_bins` x backend combination before and after (50
  fingerprints, ~2.6M values, all identical).
* Documented `\value` claims are now asserted as executable contracts, so Rd
  prose cannot drift from the code in silence. The 36 exports whose `\value`
  makes a specific structural promise -- named columns, a single row, an
  attached `"model"` object, a length matching the input -- are covered; the
  rest return a `ggplot`, a layer or a widget and are checked by their own
  tests. Every claim audited was already accurate; the tests keep it that way.
* The four exports that had no test call site at all -- `wdi_search()`,
  `clear_wdi_cache()`, `animate_world()` and `cartogram_map()` -- are covered,
  including `cartogram_map(type = "contiguous")` (the default type, previously
  never exercised) and `wdi_search()`'s zero-match and single-match paths.
* The README's "optional features at a glance" table is corrected against what
  the code actually gates on: `spin_globe()` was listed as needing only `maps` +
  `mapproj` when it also hard-requires `gifski` or `magick`; the `ggsql` row
  overstated the requirements of `as_ggsql_source()` (which never needs `ggsql`)
  and understated the version `interactive_map(engine = "ggsql")` needs; and
  `locate_country()`, `flow_map()` and `bubble_map()` were missing.
* The *countryatlas and ggsql* vignette said `DRAW spatial` "was added in 0.4.1"
  as plain fact; it now says that version is newer than what CRAN ships, which
  is why the query-executing chunks are shown but not evaluated.
* The package-level overview (`?countryatlas`) was missing `simplify_geometry()`
  and `clear_wdi_cache()` from its section list, though `_pkgdown.yml` had both.
* The set of ISO codes the package treats as countries was computed in two
  places (name matching and the World Bank aggregate filter); it now comes from
  one internal helper, which region resolution uses as well, so the three
  callers cannot drift apart.
* New offline test suites pin the things a structural test cannot: closed-form
  anchors for the hand-rolled numerical kernels (haversine distance, spherical
  polygon area, great-circle interpolation, Gini/Theil, sigma and beta
  convergence, Moran's I against an independently built weights matrix) and
  internal-consistency checks on every bundled dataset (no duplicate or unknown
  `iso3c`, coordinates in range, one country per `world_tiles` cell,
  `historical_codes` in step with its alias table, and `country_meta`
  centroids still agreeing with `polygon_centroids()`).
* README and vignettes now demonstrate every exported function:
  `wdi_search()`, `country_codes()`, `complete_years()`, `growth_rate()` /
  `index_to()`, `repair_country_names()`, `country_join_all()`,
  `locate_country()` and `facet_map()` gained worked examples, and the
  vignettes prefer `country_overrides()` over the soft-deprecated
  `wdj_overrides()`. The README's rendered output and figures were stale
  (pre-dating the quantile-breaks fix and the `gdp_per_capita_2015` opt-in)
  and have been re-rendered from the 2.0.0 code.
* `geofacet` is dropped from `Suggests`: no code ever used it, and
  `?tile_map` / the README claimed a `geofacet`-backed small-multiples
  feature that did not exist. Facet a `tile_map()` like any other `ggplot`,
  or use `facet_map()` for choropleth small multiples.
* The README's figures are shipped in the tarball again, so the images on the
  CRAN package page resolve. `.Rbuildignore` excluded the generated `.png`s
  but not the (much larger) `.gif`, which left six of the seven images
  broken.
* `?world_snapshot`'s `@format` said "two elements" while listing three, and
  advertised an `sf` element that is `NULL` in the released package. It now
  documents what actually ships and points at `attach_geometry()` for
  geometry.
* `?attach_geometry` documents which countries each geometry backend actually
  carries. Rows with no matching geometry are dropped silently, and the `sf`
  backend's `scale` changes *which* countries exist rather than only how
  detailed they are: of the 215 countries in `world_snapshot`, the default
  `scale = "small"` (110m) maps 169 and `scale = "medium"` maps 214. Five
  territories (Gibraltar, Hong Kong, Macao, Tuvalu, the British Virgin
  Islands) are in no backend at any scale.
* `?world_tiles` and `?tile_map` said "one square per country" without saying
  how many. The grid is the 239 `country_meta` rows that have a bundled
  centroid, so the 10 without one have no tile and `tile_map()` silently drops
  `data` rows keyed on them. Both are now documented, and a test pins the
  grid to that definition.

# countryatlas 1.0.0

A single, comprehensive release that takes the package from a one-function proof
of concept to a complete toolkit for joining world data to maps. The spirit is
unchanged — *ISO codes as the universal join key, one call to a map-ready
table* — but pushed to its full potential.

## Breaking-ish changes

* `world_data()` is generalised but backward-compatible: `world_data(2020)`
  still returns the classic polygon-backed, GDP-per-capita tibble. The only
  visible change is the column name `gdp_per_capita_2015` → `gdp_per_capita`.
  A one-cycle deprecation shim keeps `gdp_per_capita_2015` available as an alias
  (toggle with `options(countryatlas.gdp_compat = FALSE)`).
* The 16 regions the previous version silently dropped (Kosovo, Micronesia, the
  Virgin Islands, Saint Martin, Bonaire/Saba/Sint Eustatius, the Canary Islands,
  Madeira/Azores, …) are now **matched** via [`wdj_overrides()`] instead of
  deleted, so they appear on maps. Diffs of map output will show increased
  coverage.

## New: core data assembly

* `world_data()` gains `indicator` (one or many WDI codes; named vectors drive
  clean column names), multi-year **panels**, an `sf` backend
  (`geometry = "sf"`), `region` subsetting, `latest`, projections and caching.
* `country_data()` — the lightweight, one-row-per-country analysis table.
* `world_geometry()` — projected, region-subset geometry (countries, centroids,
  coastline, borders, graticule, ocean).

## New: the join engine (exposed for *your* data)

* `standardize_country()`, `join_world()`, `attach_geometry()`, `country_join()`.

## New: diagnostics

* `check_country_match()`, `wdj_overrides()`, `audit_coverage()` — never lose a
  country silently.

## New: reference data & translation

* `convert_country()` (flags, currency, tld, research codes), `country_codes()`,
  `country_groups()` / `in_group()`, `wdi_search()`.
* Bundled datasets: `world_snapshot`, `country_meta`, `common_indicators`,
  `country_groups_tbl`, `world_tiles`.

## New: analysis helpers

* `per_capita()`, `aggregate_regions()`, `rank_countries()`, `complete_years()`.

## New: visualization

* `world_map()` (continuous / binned / quantile / jenks / categorical),
  `bubble_map()`, `bivariate_map()`, `cartogram_map()`, `tile_map()`,
  `flow_map()`, `animate_world()`, `interactive_map()`, `geom_country_labels()`,
  `theme_world_map()`.

## Performance & offline

* WDI fetches are memoised with an optional on-disk cache; multiple indicators
  are fetched in parallel (`parallel::mclapply`) where supported.
  See `clear_wdi_cache()`.
* The bundled `world_snapshot` lets every example, test and vignette run offline
  and deterministically.

## Engineering

* Namespace hygiene (targeted `@importFrom` instead of blanket `@import`).
* Input validation with friendly `cli` / `rlang` errors.
* A `testthat` (3e) suite; network calls are skipped offline and on CRAN.
* Vignettes and a `pkgdown` site.
* Refreshed CI: R-CMD-check, test-coverage and pkgdown workflows.
* Heavy spatial dependencies (`sf`, `rnaturalearth`, `cartogram`, `biscale`,
  `geofacet`, `gganimate`, `leaflet`, …) are all in `Suggests` and gated by
  `rlang::check_installed()`, so the base install stays light.

Group memberships in `country_groups_tbl` are point-in-time as of 2024-01-01.

# countryatlas 0.1.0

* Initial experimental release with a single `world_data(year)` function.
