CountryAtlas API — reference
Base URL: https://www.countryatlas.co/api/v1 (proxied by the Next.js app to the FastAPI process on 127.0.0.1:8291).
Interactive docs: /api/v1/docs (Swagger) · /api/v1/redoc · spec /api/v1/openapi.json.
- All endpoints are
GET(exceptPOST /admin/refresh,POST /admin/cache/clear) and return JSON (gzip when accepted). - Identifiers: countries by ISO3 or slug (case-insensitive:
CAN,can,canada); indicators, topics and groups by slug. - Every response carries
meta: {built_at, run_id, generated_at}; the headerX-CountryAtlas-Runrepeats the snapshot run id andX-Cache: HIT|MISStells whether the in-process cache served it (cache keys include the run id, so a new snapshot invalidates everything). - Errors are RFC 7807
application/problem+json:{"type","title","status","detail","instance",…}.404unknown country/indicator/topic/group (with a helpfuldetail),400bad combination,422invalid parameter (witherrors[]),429rate limit (120 req/min/IP,Retry-After),503 {"title":"Data not built yet"}while no snapshot exists. - Run locally:
CA_DATA_DIR=~/countryatlas-data ca-api(orpython -m uvicorn countryatlas.api.main:app --port 8291).
The provenance object
Every value the API returns (headline metrics, series points, ranking rows, map values, curated lists, changes…) carries a provenance
object built from observations → indicator_sources → sources:
{
"value": 55697.66, "period": "2025-01-01", "year": 2025, "unit": "current US$", "is_estimate": false, "is_forecast": false,
"status": "imported", "formatted": "55.7k",
"provenance": {
"source": "worldbank", "source_name": "World Bank", "dataset": "WDI", "series_code": "NY.GDP.PCAP.CD",
"retrieved_at": "2026-09-11T06:49:30Z", "source_updated_at": "2026-07-13T00:00:00Z",
"url": "https://data.worldbank.org/indicator/NY.GDP.PCAP.CD?locations=CA", "transform": null, "licence": "CC BY 4.0"
}
}| field | meaning |
|---|---|
source / source_name |
connector id (worldbank, imf, oecd, eurostat, who, fred, owid, bis, ilo) and display name |
dataset, series_code |
dataset and series inside the source (WDI NY.GDP.PCAP.CD, WEO NGDPDPC, OWID co2_per_capita, FRED FEDFUNDS…) |
retrieved_at |
when the pipeline fetched the raw payload (UTC) |
source_updated_at |
last update advertised by the source (may be null when the source does not publish it) |
url |
deep link: World Bank …/indicator/{code}?locations={iso2}, OWID grapher ourworldindata.org/grapher/{slug} or the owid/co2-data / owid/energy-data repos, Eurostat databrowser, WHO GHO indicator page, FRED series page, IMF / OECD / BIS / ILO data portals |
transform |
expression applied at normalisation (e.g. x*1e9), null if none |
licence |
licence of the source |
Series responses also list all sources used (sources[], with n_values) and the dominant one as provenance.
Endpoints
Health
GET /health → {status: "ok"|"empty", run_id, built_at, observations, countries, indicators, version, cache}/health never returns 503; status: "empty" means the pipeline has not produced atlas.duckdb yet.
Countries
GET /countries?region=&income=&q=&sort=name|population|gdp|gdp_per_capita|coverage&kind=&limit=&offset=
GET /countries/{id} overview: country header, groups, coverage, freshness, headline metrics, topics summary, neighbours
GET /countries/{id}/topics/{topic} indicators of a topic grouped by subtopic (registry order); indicators without data → has_data:false
GET /countries/{id}/series/{indicator}?from=&to=&freq=A|Q|M&include_forecast=true&include_alt=false
GET /countries/{id}/changes?limit=&kind= recent detected changes (by severity)
GET /countries/{id}/events?limit=&kind=&indicator= whole-history timeline
GET /countries/{id}/similar?mode=overall|economic|demographic|energy|social&limit=
GET /countries/{id}/insights
GET /countries/{id}/dna
GET /countries/{id}/download.csv|json?include_forecast=&topic=region accepts any group id or slug (ecs, europe-central-asia, oecd, g7…); income accepts HIC|UMC|LMC|LIC or the slug.
curl -s https://www.countryatlas.co/api/v1/countries/canada | jq '.headline[] | {indicator, formatted, year, rank_world, n_world, source: .provenance.source}'
# {"indicator":"gdp-per-capita","formatted":"55.7k","year":2025,"rank_world":21,"n_world":188,"source":"worldbank"} …
curl -s "https://www.countryatlas.co/api/v1/countries/CAN/series/gdp-per-capita?from=2000" | jq '.stats, .provenance'Headline metric object (MetricValue): value, formatted, period, year, unit, is_estimate, is_forecast, status, prev{period,value}, change{abs,pct,formatted}, change_10y{…}, rank_world/n_world, rank_region/n_region (World Bank region), rank_income/n_income, rank_year, rank_is_stale (rank computed on a year > 2 years older than the indicator's latest year), higher_is_better, sparkline [[year, value]…] (last 30 non-forecast points), provenance.
Indicators
GET /indicators?topic=&q=&featured=&source=&with_data=
GET /indicators/{slug} definition, sources (priority order, deep links), coverage (+ by_year), world_latest, freshness, top5/bottom5, years, topics
GET /indicators/{slug}/map?year=&nearest=false&classes=6
GET /indicators/{slug}/trend?group=world&from=&to=&min_n=5
GET /indicators/{slug}/download.csv|json?from=&to=&include_forecast=world_latestis computed across countries (World Bank aggregates such as WLD are not stored):kindissumfor additive indicators,weighted_mean(population-weighted) for per-capita / share indicators,medianotherwise;median,mean,weighted_meanandnare always returned.map: withoutyear, the latest year with ≥ 50 countries is used (year_used). Withnearest=trueeach country's latest value within 3 years of the reference year is used andyears{ISO3: year}says which.legend.breaksare quantile breaks (5–7 classes).trend: per yearmedian,mean,weighted_mean(population) for per-capita/percent indicators,sumfor additive ones,n;preferredsays which to plot.
curl -s "https://www.countryatlas.co/api/v1/indicators/life-expectancy/map?year=2023" | jq '{year_used, n, legend, CAN: .values.CAN}'
curl -s "https://www.countryatlas.co/api/v1/indicators/gdp/trend?group=oecd" | jq '.points[-1]'Series bundle
GET /series?country=CAN,FRA&indicator=gdp-per-capita,inflation&from=1990&to=2026&freq=A&include_forecast=trueReturns series[] (one per indicator × country), each with values[] (per-point provenance), sources[], provenance, stats{min,max,first,last,cagr}.
Rankings
GET /rankings?topic= rankable indicators (featured first) with ranking_year / ranking_n
GET /rankings/{indicator}?year=&group=world&sort=asc|desc&limit=50&offset=0&sparkline=true
GET /rankings/{indicator}/history?countries=CAN,USA&from=&to=Rows: rank (within the requested group), rank_world/n_world (from the snapshot), pct_rank, country{id,slug,name,flag,region,income},
value, formatted, year, change_1y, change_10y, sparkline, provenance. Default sort is asc when higher_is_better=false
(e.g. infant mortality), otherwise desc. year_used falls back to the nearest available year; years_available lists them.
curl -s "https://www.countryatlas.co/api/v1/rankings/gdp-per-capita?limit=3" | jq '.rows[] | [.rank, .country.name, .formatted, .change_1y.formatted]'
# [1,"Luxembourg","147.3k","+6.9 %"] [2,"Ireland","131.6k","+16.6 %"] [3,"Switzerland","114.8k","+6.6 %"]Compare
GET /compare?countries=CAN,USA,FRA&indicators=gdp,gdp-per-capita&from=&to=&mode=absolute|per-capita|index100|pct&include_forecast=
GET /compare/snapshot?countries=CAN,USA&topic=economy (or &indicators=a,b; default = headline indicators)
GET /compare/download.csv|json?countries=&indicators=&from=&to=Modes: index100 rebases each series to 100 at the first available year ≥ from; per-capita divides additive (aggregation=sum)
indicators by the population series of the same year (already per-capita indicators are left unchanged, transform.applied=false);
pct = % change vs previous period. snapshot returns one row per indicator with a values{ISO3: MetricValue} map and best (when higher_is_better is known).
Regions / groups
GET /regions?kind=world|region|continent|income|org
GET /regions/{slug}?indicator=gdp-per-capita&sort=asc|descGroup page: members with headline values (+ provenance), aggregates (sum population/GDP, population-weighted GDP per capita, median life
expectancy…, each with kind), and a member ranking on the chosen indicator.
Search
GET /search?q=&limit=10&type=country|indicator|topic|region|sourceTyped hits {type, id, slug, name, hint, score, url} from search_index (exact → prefix → word → substring → Jaro-Winkler fuzzy).
Hints: Country · North America, Indicator · Economy · annual %, Topic · 12 indicators, Region · Organisation · 38 members.
Two-word combos such as housing canada or canada gdp also return country_topic / country_indicator hits with a ready URL
(/countries/canada/housing).
Home
GET /homesnapshot (world population / GDP sums, median life expectancy, counts, built_at), lists (largest economies; fastest GDP growth,
fastest population growth, highest life expectancy and energy transition leaders among countries ≥ 1 M; highest GDP per capita PPP;
lowest unemployment among countries ≥ 5 M — 8 rows each with provenance, filter_note), recent_changes (12 most severe),
recently_updated, featured_indicators, trending.
Changes, sources, methodology
GET /changes?limit=&offset=&kind=&indicator=&country=&topic=&min_severity=
GET /sources n_observations, n_indicators, licence, last_retrieved_at
GET /sources/{id} indicators mapped, datasets, import_runs, freshness
GET /methodology registry-derived: topics, units/formats, source priority rule + URL patterns, validation rules, derived computations, DNA dimensionsDownloads
CSV is streamed with a leading # CountryAtlas export · run … · built … comment line and the columns
country_id, country_name, indicator_id, indicator_name, period, year, frequency, value, unit, is_estimate, is_forecast, status, source, source_name, dataset, series_code, retrieved_at, source_updated_at, url, licence. JSON returns {meta, n, columns, rows}.
Admin (header X-Admin-Token: $CA_ADMIN_TOKEN)
GET /admin/overview db, meta, table counts, connectors health (import_runs), sources freshness/stale counts, scheduler heartbeat (data_dir/scheduler.json + scheduler.pid), cache stats
GET /admin/runs?limit=&connector=&status=
GET /admin/issues?severity=&connector=&indicator=&code=&run_id=&limit=
GET /admin/coverage indicator × n_countries / last_year matrix + per-country coverage
GET /admin/raw?run_id= raw files stored for a run
POST /admin/refresh sends SIGUSR1 to the scheduler pid (409 when no scheduler)
POST /admin/cache/clear403 on a wrong/missing token, 503 {"title":"Admin disabled"} when CA_ADMIN_TOKEN is not set.
Operations notes
- Snapshot swap: the API opens
~/countryatlas-data/atlas.duckdbread-only and comparesst_ino/st_mtime_nson every request; when the pipelineos.replace()s a new file, the old connection is closed and the new one opened (DuckDB caches instances per path, so the close must happen first). Responses are cached in-process per(run_id, path, query); nothing survives a new run id. - Performance (real snapshot, 2.0 M observations): country overview ≈ 12 ms warm (≈ 170 ms on the very first request while static lookups load), rankings ≈ 20 ms, home ≈ 33 ms, map ≈ 4 ms, cached hits ≈ 1 ms.
- Formatting:
formattedstrings use the indicator'sformat: currency compact with the registryunit_shortprefix (US$53.4k,US$1.2T,intl $45.3B), number compact (41.7M), percent3.4 %, years82.1 yrs, tonnes5.2 t, per-10003.2 per 1,000, per-100k1.2 per 100k.formatted_shortis the same value without prefix/unit (53.4k,3.4) for tight cells. - Registry reloads: the headline list (
topics.yaml), indicator metadata and groups are re-read from the YAML registry each time a new snapshot run id is opened (the loaders'lru_cacheis cleared), so registry edits take effect with the next build. - Home lists: each curated list carries
min_populationandfilter_note(e.g."Countries above 1M inhabitants") when a population floor applies (fastest GDP growth, fastest population growth, highest life expectancy, energy transition leaders: ≥ 1M; lowest unemployment: ≥ 5M).
Analytics endpoints (API 1.1 — contract, 2026-09-11)
All additive, read-only, GET, cached per snapshot like the rest. Paths stay under /api/v1. Country-only pools (kind='country'),
annual canonical frequency, non-forecast, non-quarantined. Every value object still carries provenance where a single source
applies; aggregates carry provenance[] (one per contributing source, with n_values). Neutral wording everywhere: a change is an
increase / decrease; it is called an improvement / deterioration only when the indicator declares higher_is_better.
GET /pulse World Pulse — what is changing globally (latest year vs previous)
GET /movers?window=1|5|10&category=&kind=&limit=&min_population= Biggest movers by window / category / kind
GET /extremes?window=1|5|10|25|since1990&topic=&min_population= Curated extremes facets (fastest ageing, urbanising…)
GET /scatter?x=&y=&size=&year=&group=&log_x=&log_y= Cross-section scatter + Pearson / Spearman / OLS
GET /trajectory?x=&y=&size=&from=&to=&group= Gapminder-style frames (compact arrays per country)
GET /finder?f=slug:op:value&f=…&mode=and|or®ion=&income=&sort=&limit= Structured country finder over `latest`
GET /peers?y=&x=&year=&method=theil-sen|ols&log_x= Above / below expected (robust cross-sectional fit)
GET /indicators/{slug}/related?limit=&min_n= Statistically related indicators (descriptive)
GET /indicators/{slug}/distribution?year=&highlight=&bins= Histogram + medians (world / region / income) + percentile
GET /indicators/{slug}/frames?from=&to=&step=&group= Multi-year map frames for the time machine
GET /indicators/{slug}/quality Coverage / freshness / continuity summary
GET /rankings/{indicator}/race?from=&to=&top=&group= Rank race frames (top N per year)
GET /regions/compare?a=&b=&indicators= Group vs group aggregates + history
GET /countries/{id}/story "How X changed": long-run indicators, templated text
GET /countries/{id}/dna?reference=world|region|income|<ISO3> DNA + reference profile
GET /countries/{id}/quality Per-indicator data quality for a country
GET /updates Freshness dashboard (sources, runs, changed values)
GET /search?q=compare canada usa Intent hits (type action) — see belowShapes
MoverItem — {country: CountryCard, indicator: IndicatorCard, kind, year, ref_year, value, ref_value, delta, delta_pct, formatted, formatted_ref, severity (0–1), direction: "up"|"down", interpretation: "improvement"|"deterioration"|null, headline, provenance}.
kind ∈ yoy_jump | yoy_drop | record_high | record_low | n_year_high | n_year_low | sign_flip | accelerating | decelerating | structural_break | trend_reversal | volatility_spike | change_5y | change_10y. Categories: economic = economy+government+trade+income,
demographic = population, health, energy, climate = climate+environment, digital = digital+innovation, housing, labor.
Kinds filter: improvement | deterioration | increase | decrease | record | reversal | acceleration | structural | all.
/pulse → {meta, year_reference, summary: {n_indicators, n_countries_reporting, n_record_highs, n_record_lows, n_changes}, items: [{indicator, year, n, n_up, n_down, n_flat, share_up, share_down, median_change_abs, median_change_pct, direction_semantics: "higher_is_better"|"lower_is_better"|"neutral", record_highs, record_lows, headline, top_up: MoverLite, top_down: MoverLite, convergence: {direction, cv_start, cv_end, n, from_year} | null, provenance}]} where MoverLite = {country, value, ref_value, delta, delta_pct, formatted, year}. Only headline + featured indicators whose latest year is ≥ reference − 1. Headlines are templates, e.g.
"Inflation fell in 73 % of 176 reporting countries" (share_down ≥ 60), "Population is shrinking in 31 countries", "Renewable
electricity hit a record high in 42 countries".
/extremes → {meta, window, from_year, to_year, min_population, filter_note, facets: [{id, title, indicator, direction: "up"|"down", metric: "abs"|"pct"|"points", rows: [{country, value_start, value_end, year_start, year_end, delta, delta_pct, formatted_start, formatted_end}], n, provenance}]}. Facets (fixed, in this order, skipped when the indicator has < 30 countries in the window):
aging median-age ↑ · urbanizing urban-population-share ↑ · fertility-decline fertility-rate ↓ · life-expectancy-gains ↑ ·
gdp-transformations gdp-per-capita-ppp ↑ (pct) · digital-adoption internet-users ↑ · renewable-transitions renewable-electricity-share ↑ ·
co2-reductions co2-per-capita ↓ (pct) · population-decline population ↓ (pct) · population-boom population ↑ (pct) ·
inflation-surges inflation ↑ (points) · debt-buildup general-government-gross-debt-pct-gdp ↑ (points) · unemployment-falls ↓ (points).
topic filters facets by their indicator topic. window=since1990 → from_year 1990.
/scatter → {meta, x, y, size, year, year_used, nearest_years: 3, group, n, points: [{id, slug, name, flag, region, income, x, y, size, year_x, year_y}], stats: {n, pearson, spearman, ols: {slope, intercept, r2} | null, log_x, log_y}, note}. log_x/log_y accept
true|false|auto (auto = true when the indicator's format is currency/number with bounds ≥ 0 and max/min > 50). size defaults to
population; size=none disables. Each axis uses the country's value in year_used or the nearest within 3 years.
/trajectory → {meta, x, y, size, group, years: [int…], countries: [{id, slug, name, flag, region, income}], series: {ISO3: {x: [num|null…], y: [...], size: [...]}}, domains: {x: [min,max], y: [min,max], size: [min,max]}, log_x, log_y, provenance: [...]} — arrays aligned on
years, no interpolation. Default from = first year with ≥ 50 countries on both axes, to = last year with ≥ 50. Countries with
< 30 % of frames on both axes are dropped.
/finder → filters f=slug:op:value (repeatable; ops gt gte lt lte eq between — between uses a..b), mode=and|or (default and),
region/income group slug, sort=slug:asc|desc (default: first filter, desc), limit ≤ 218. Response {meta, mode, filters: [{indicator, op, value, value2, year_used}], n_matching, n_evaluated, items: [{country: CountryCard, matched: [slug…], values: {slug: {value, year, formatted, provenance}}}]}. year_used = latest year of each country (mixed years, honest).
/peers → {meta, x, y, year_used, n, method, fit: {slope, intercept, r2, log_x, residual_scale}, points: [{id, slug, name, flag, region, income, x, y, expected, residual, residual_z}], above: [top 12 by residual_z], below: [bottom 12], pairs: [{x, y, label}], note, methodology}.
residual_z = residual / (1.4826·MAD of residuals). Default pair y=life-expectancy, x=gdp-per-capita-ppp, log_x=true. Suggested pairs:
(life-expectancy, gdp-per-capita-ppp), (expected-years-of-schooling, gdp-per-capita-ppp), (co2-per-capita, gdp-per-capita-ppp),
(life-expectancy, health-expenditure-per-capita), (internet-users, gdp-per-capita-ppp), (infant-mortality-rate, gdp-per-capita-ppp).
Wording must stay descriptive: "above the fitted line", never "outperforms because".
/indicators/{slug}/related → {meta, indicator, year_used, n_candidates, items: [{indicator, pearson, spearman, n, year, log_x, log_y, direction: "positive"|"negative"}], note: "Correlation does not imply causation."} — cross-section on latest (values within 3 years of
the indicator's max year), pairs need n ≥ min_n (default 40), sorted by |Spearman| desc, self and per-capita twins of the same
quantity excluded when both are present (e.g. gdp vs gdp-ppp is allowed; gdp-per-capita vs gdp-per-capita-ppp is allowed — no
hand rules beyond self).
/indicators/{slug}/distribution → {meta, indicator, year, year_used, n, log, histogram: {edges, counts, log}, stats: {min, p10, p25, median, mean, p75, p90, max}, highlight: {country: CountryCard, value, percentile, rank, n, region: GroupCard|null, region_median, income: GroupCard|null, income_median} | null, by_region: [{group: GroupCard, median, n}], by_income: [{group, median, n}], provenance}.
/indicators/{slug}/frames → {meta, indicator, group, years: [...], values: {ISO3: [num|null…]}, legend: {min, max, breaks: [...], n_classes}, n_by_year: [...], provenance} — breaks are quantiles over the pooled values of all years (a stable legend while scrubbing).
Years limited to those with ≥ 20 countries; ≤ 80 frames.
/indicators/{slug}/quality → {meta, indicator, n_countries, n_countries_total, coverage_pct, first_year, last_year, latest_common_year, n_years, years_with_50plus, median_points_per_country, sparse_countries, stale_countries, flagged_values, sources: [...], badges: [...]}.
Badges vocabulary (shared with the country endpoint): fresh (latest year ≥ reference − 1), historical (first year ≤ 1970),
sparse (median points per country < 10), limited-coverage (< 50 % of countries), stale (latest year ≤ reference − 3), flagged
(> 5 % of values with status warning), forecast (chosen source publishes projections).
/rankings/{indicator}/race → {meta, indicator, group, top, years: [...], frames: [{year, rows: [{id, value, rank}]}], countries: {ISO3: CountryCard}, max_value, provenance}. Union of countries that appear in the top N in any frame; only years with ≥ 20 ranked countries.
/regions/compare → {meta, groups: [GroupCard, GroupCard], rows: [{indicator, kind: "sum"|"median"|"weighted_mean", label, values: {<group id>: {value, formatted, n, year}}}], shares: {<group id>: {population_share_pct, gdp_share_pct}}, history: {<indicator slug>: {years: [...], <group id>: [num|null…]}}} for indicators (default) population, gdp, gdp-per-capita, life-expectancy, co2-per-capita, internet-users,
gdp-growth, inflation; history for the first four (sum/weighted/median per year, ≥ 60 % of members reporting).
/countries/{id}/story → {meta, country, since, items: [{indicator, first: {year, value, formatted}, last: {year, value, formatted}, change_abs, change_pct, cagr, peak: {year, value}, trough: {year, value}, rank_first: {rank, n, year} | null, rank_last: {...} | null, series: [[year, value]…], text, provenance}]} — indicators in this order when ≥ 10 annual points: population, gdp-per-capita-ppp
(fallback gdp-per-capita), life-expectancy, fertility-rate, urban-population-share, co2-per-capita, internet-users,
renewable-electricity-share, general-government-gross-debt-pct-gdp, median-age, energy-use-per-capita, unemployment-rate; max 10 items.
text template: "{country}'s {indicator} {rose|fell} from {first} in {y0} to {last} in {y1} ({signed pct} / {signed points})."
/countries/{id}/dna (extended) → adds reference: {kind: "world"|"region"|"income"|"country", id, label, dims: {...}} | null
(world → 50 on every dimension; region/income → median of the members' dims; country → that country's dims).
/countries/{id}/quality → {meta, country, summary: {n_indicators, n_with_data, coverage_pct, latest_year, n_fresh, n_stale, n_sparse, n_flagged}, items: [{indicator, latest_year, first_year, n_points, expected_points, missing_years, continuity_pct, status, source, source_updated_at, retrieved_at, badges: [...]}]}.
/updates → {meta, snapshot: {run_id, built_at, observations, indicators, countries, values_changed, values_changed_by_source: {...}}, sources: [{source: Source, status: "ok"|"partial"|"failed"|"stale"|"unknown", last_success_at, last_retrieved_at, source_updated_at, n_datasets, n_indicators, n_observations, latest_year, values_changed, countries_affected}], recent_runs: [{run_id, connector, dataset, started_at, finished_at, status, rows_valid, warnings, errors, message}], indicators_recently_updated: [IndicatorSummary…]}.
No file paths, hosts or secrets in the payload.
/search intents — deterministic parsing before the index lookup; each intent yields a hit {type: "action", action, id, name, hint, url, score: 1.0}: compare <c1> <c2> [<c3>…] → /compare/<slugs>; rank[ing] <indicator> → /rankings/<slug>; <indicator> <country>
(already country_indicator); <indicator> <group> → /rankings/<slug>?group=<group>; <indicator> map|explore → /explore?indicator=;
<c1> vs <c2> → compare.
Change detection 2.0 kinds (pipeline)
structural_break (single mean shift with gain ≥ 0.5 and shift ≥ 1.5 × series sd, segments ≥ 5 years, break within the last 10 years for
changes), trend_reversal (three consecutive yearly moves of one sign after three of the opposite sign), volatility_spike (sd of the
last 5 yearly differences ≥ 3 × the sd of the previous 15). Same row shape; detail documents the parameters.
Implementation notes (as built, 2026-09-12 — deviations from the contract above)
/pulsetakesmin_population(default 1 000 000,0disables) that filters the top movers only (top_up/top_down); counts and shares use every reporting country. The response echoesmin_populationandfilter_note. Pools need ≥ 30 reporting countries (not 20) so a thin indicator (Gini, 25 countries) never headlines;year_referenceis the latest year of the annual pillars population / GDP / life expectancy (monthly/quarterly series already carry the current year). Convergence compares max-year vs max-year − 10./moverswindows 5/10 report kindschange_5y/change_10y(direction indirection), keep moves with robust |z| ≥ 1.5 and rank by severity = min(1, |z|/4) × importance;kind=record|reversal|acceleration|structuralare only meaningful forwindow=1and return an empty list otherwise. Window 1 reads thechangestable (any kind, including the pipeline's structural_break / trend_reversal / volatility_spike)./extremesfacets each carry their ownfrom_year/to_year(the indicator's latest common year and its window start); the top-level pair is the min/max across facets. Start values use the nearest observation within ±2 years of the window start./indicators/{slug}/related:min_naccepts ≥ 5 (default 40). Pearson is computed on the association scale (log10 for level series, flaggedlog_x/log_y), Spearman on raw values./indicators/{slug}/distribution:highlightalso returnsyear; group medians need ≥ 2 members with data./regions/comparehistory[slug]also carrieskind;/updatessources carryruns {n, n_failed}andsnapshot.reference_year./countries/{id}/storytext template is"{Country}: {Indicator} rose|fell from {first} in {y0} to {last} in {y1} ({signed change}[, ±x % a year])."(colon form keeps acronyms such as GDP / PPP intact);/countries/{id}/dnaaddsnote./searchintents: countries are resolved by ISO2/ISO3 code first, then exact single word, then (near-)exact 2–3-word names, then fuzzy.