diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..95ede81
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,4 @@
+# Preserve hash-pinned publisher bytes, including original line endings/spacing.
+db/data/stats_nz/subnational_population_estimates_2025/subnational-population-estimates-at-30-june-2025-provisional.xlsx binary
+db/data/stats_nz/regional_council_2025_codes/regional-council-2025-codes.json binary
+db/data/stats_nz/national_population_estimates_2025/national-population-estimates-at-30-june-2025-landing-page.html binary
diff --git a/changelog.d/nz-partial-population-2025.added.md b/changelog.d/nz-partial-population-2025.added.md
new file mode 100644
index 0000000..34fbf58
--- /dev/null
+++ b/changelog.d/nz-partial-population-2025.added.md
@@ -0,0 +1,5 @@
+Add 85 source-backed, provisional Stats NZ population counts for 30 June 2025,
+covering the 16 regions and the separately published national total across all
+ages and four broad age groups. Pin publisher artifacts and REGC 2025 codes.
+Keep five-year-age-by-sex regional coverage and national single-year-age coverage
+explicitly incomplete; no calibrated population or completed target set is added.
diff --git a/chronicle/source_package.py b/chronicle/source_package.py
index 993b4d9..1e5bdfb 100644
--- a/chronicle/source_package.py
+++ b/chronicle/source_package.py
@@ -74,6 +74,9 @@
SOURCE_PACKAGE_RESOURCE_PACKAGE = "packages"
SOURCE_PACKAGE_ALIASES = {
+ "stats-nz-subnational-population-estimates-2025": Path(
+ "stats_nz/subnational_population_estimates_2025"
+ ),
"bea-nipa-personal-income-components": Path("bea/nipa_personal_income_components"),
"bea-nipa-personal-income-disposition": Path(
"bea/nipa_personal_income_disposition"
diff --git a/db/data/stats_nz/national_population_estimates_2025/manifest.yaml b/db/data/stats_nz/national_population_estimates_2025/manifest.yaml
new file mode 100644
index 0000000..bb85b90
--- /dev/null
+++ b/db/data/stats_nz/national_population_estimates_2025/manifest.yaml
@@ -0,0 +1,19 @@
+source_id: stats_nz
+package_id: national_population_estimates_2025
+dataset: stats_nz_national_population_estimates_2025
+source_page: https://www.stats.govt.nz/information-releases/national-population-estimates-at-30-june-2025/
+table: 'National population estimates: At 30 June 2025 (landing-page evidence only;
+ no single-year-age artifact)'
+files:
+ 2025:
+ filename: national-population-estimates-at-30-june-2025-landing-page.html
+ source_url: https://www.stats.govt.nz/information-releases/national-population-estimates-at-30-june-2025/
+ sha256: b2d2751800857895b1498d87df6f03ac3511ec8c7a9b17cc3150c8ab56533053
+ size_bytes: 61286
+ fetched_at: '2026-08-30T01:43:59+00:00'
+ storage:
+ r2:
+ provider: r2
+ bucket: ledger-raw
+ key: raw/nz/stats_nz/national_population_estimates_2025/2025/b2d2751800857895b1498d87df6f03ac3511ec8c7a9b17cc3150c8ab56533053/national-population-estimates-at-30-june-2025-landing-page.html
+ uri: r2://ledger-raw/raw/nz/stats_nz/national_population_estimates_2025/2025/b2d2751800857895b1498d87df6f03ac3511ec8c7a9b17cc3150c8ab56533053/national-population-estimates-at-30-june-2025-landing-page.html
diff --git a/db/data/stats_nz/national_population_estimates_2025/national-population-estimates-at-30-june-2025-landing-page.html b/db/data/stats_nz/national_population_estimates_2025/national-population-estimates-at-30-june-2025-landing-page.html
new file mode 100644
index 0000000..c6b5be5
--- /dev/null
+++ b/db/data/stats_nz/national_population_estimates_2025/national-population-estimates-at-30-june-2025-landing-page.html
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+National population estimates: At 30 June 2025 | Stats NZ
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/db/data/stats_nz/regional_council_2025_codes/manifest.yaml b/db/data/stats_nz/regional_council_2025_codes/manifest.yaml
new file mode 100644
index 0000000..ef09675
--- /dev/null
+++ b/db/data/stats_nz/regional_council_2025_codes/manifest.yaml
@@ -0,0 +1,18 @@
+source_id: stats_nz
+package_id: regional_council_2025_codes
+dataset: stats_nz_regional_council_2025_codes
+source_page: https://services2.arcgis.com/vKb0s8tBIA3bdocZ/arcgis/rest/services/Regional_Council_2025/FeatureServer
+table: Regional council 2025 code and name attributes (1 January 2025 boundaries)
+files:
+ 2025:
+ filename: regional-council-2025-codes.json
+ source_url: https://services2.arcgis.com/vKb0s8tBIA3bdocZ/arcgis/rest/services/Regional_Council_2025/FeatureServer/0/query?where=1%3D1&outFields=REGC2025_V1_00%2CREGC2025_V1_00_NAME&returnGeometry=false&orderByFields=REGC2025_V1_00&f=pjson
+ sha256: e5d53fa6abf742121d123bff970c77658a16e8c9b405b83d1a978cc22920d660
+ size_bytes: 3381
+ fetched_at: '2026-08-30T01:43:16+00:00'
+ storage:
+ r2:
+ provider: r2
+ bucket: ledger-raw
+ key: raw/nz/stats_nz/regional_council_2025_codes/2025/e5d53fa6abf742121d123bff970c77658a16e8c9b405b83d1a978cc22920d660/regional-council-2025-codes.json
+ uri: r2://ledger-raw/raw/nz/stats_nz/regional_council_2025_codes/2025/e5d53fa6abf742121d123bff970c77658a16e8c9b405b83d1a978cc22920d660/regional-council-2025-codes.json
diff --git a/db/data/stats_nz/regional_council_2025_codes/regional-council-2025-codes.json b/db/data/stats_nz/regional_council_2025_codes/regional-council-2025-codes.json
new file mode 100644
index 0000000..b7b0f48
--- /dev/null
+++ b/db/data/stats_nz/regional_council_2025_codes/regional-council-2025-codes.json
@@ -0,0 +1,140 @@
+{
+ "objectIdFieldName" : "OBJECTID",
+ "uniqueIdField" :
+ {
+ "name" : "OBJECTID",
+ "isSystemMaintained" : true
+ },
+ "globalIdFieldName" : "",
+ "geometryType" : "esriGeometryPolygon",
+ "spatialReference" : {
+ "wkid" : 2193,
+ "latestWkid" : 2193
+ },
+ "fields" : [
+ {
+ "name" : "REGC2025_V1_00",
+ "type" : "esriFieldTypeString",
+ "alias" : "Regional Council Code",
+ "sqlType" : "sqlTypeOther",
+ "length" : 2,
+ "domain" : null,
+ "defaultValue" : null,
+ "description" : "{\"value\":\"Regional Council unique identifier\",\"fieldValueType\":\"uniqueIdentifier\"}"
+ },
+ {
+ "name" : "REGC2025_V1_00_NAME",
+ "type" : "esriFieldTypeString",
+ "alias" : "Regional Council Name",
+ "sqlType" : "sqlTypeOther",
+ "length" : 254,
+ "domain" : null,
+ "defaultValue" : null,
+ "description" : "{\"value\":\"Regional Council name (supports macrons)\",\"fieldValueType\":\"nameOrTitle\"}"
+ }
+ ],
+ "features" : [
+ {
+ "attributes" : {
+ "REGC2025_V1_00" : "01",
+ "REGC2025_V1_00_NAME" : "Northland Region"
+ }
+ },
+ {
+ "attributes" : {
+ "REGC2025_V1_00" : "02",
+ "REGC2025_V1_00_NAME" : "Auckland"
+ }
+ },
+ {
+ "attributes" : {
+ "REGC2025_V1_00" : "03",
+ "REGC2025_V1_00_NAME" : "Waikato Region"
+ }
+ },
+ {
+ "attributes" : {
+ "REGC2025_V1_00" : "04",
+ "REGC2025_V1_00_NAME" : "Bay of Plenty Region"
+ }
+ },
+ {
+ "attributes" : {
+ "REGC2025_V1_00" : "05",
+ "REGC2025_V1_00_NAME" : "Gisborne Region"
+ }
+ },
+ {
+ "attributes" : {
+ "REGC2025_V1_00" : "06",
+ "REGC2025_V1_00_NAME" : "Hawke's Bay Region"
+ }
+ },
+ {
+ "attributes" : {
+ "REGC2025_V1_00" : "07",
+ "REGC2025_V1_00_NAME" : "Taranaki Region"
+ }
+ },
+ {
+ "attributes" : {
+ "REGC2025_V1_00" : "08",
+ "REGC2025_V1_00_NAME" : "Manawatū-Whanganui Region"
+ }
+ },
+ {
+ "attributes" : {
+ "REGC2025_V1_00" : "09",
+ "REGC2025_V1_00_NAME" : "Wellington Region"
+ }
+ },
+ {
+ "attributes" : {
+ "REGC2025_V1_00" : "12",
+ "REGC2025_V1_00_NAME" : "West Coast Region"
+ }
+ },
+ {
+ "attributes" : {
+ "REGC2025_V1_00" : "13",
+ "REGC2025_V1_00_NAME" : "Canterbury Region"
+ }
+ },
+ {
+ "attributes" : {
+ "REGC2025_V1_00" : "14",
+ "REGC2025_V1_00_NAME" : "Otago Region"
+ }
+ },
+ {
+ "attributes" : {
+ "REGC2025_V1_00" : "15",
+ "REGC2025_V1_00_NAME" : "Southland Region"
+ }
+ },
+ {
+ "attributes" : {
+ "REGC2025_V1_00" : "16",
+ "REGC2025_V1_00_NAME" : "Tasman Region"
+ }
+ },
+ {
+ "attributes" : {
+ "REGC2025_V1_00" : "17",
+ "REGC2025_V1_00_NAME" : "Nelson Region"
+ }
+ },
+ {
+ "attributes" : {
+ "REGC2025_V1_00" : "18",
+ "REGC2025_V1_00_NAME" : "Marlborough Region"
+ }
+ },
+ {
+ "attributes" : {
+ "REGC2025_V1_00" : "99",
+ "REGC2025_V1_00_NAME" : "Area Outside Region"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/db/data/stats_nz/subnational_population_estimates_2025/manifest.yaml b/db/data/stats_nz/subnational_population_estimates_2025/manifest.yaml
new file mode 100644
index 0000000..efa8567
--- /dev/null
+++ b/db/data/stats_nz/subnational_population_estimates_2025/manifest.yaml
@@ -0,0 +1,18 @@
+source_id: stats_nz
+package_id: subnational_population_estimates_2025
+dataset: stats_nz_subnational_population_estimates_2025
+source_page: https://www.stats.govt.nz/information-releases/subnational-population-estimates-at-30-june-2025/
+table: 'Subnational population estimates: At 30 June 2025 (provisional)'
+files:
+ 2025:
+ filename: subnational-population-estimates-at-30-june-2025-provisional.xlsx
+ source_url: https://www.stats.govt.nz/assets/Uploads/Subnational-population-estimates/Subnational-population-estimates-At-30-June-2025/Download-data/subnational-population-estimates-at-30-june-2025-provisional.xlsx
+ sha256: 001e8a896cfb50f5ed17836dc815b235e3bcca55ee91c9869a2afaeb054b50a6
+ size_bytes: 97990
+ fetched_at: '2026-08-30T01:38:43+00:00'
+ storage:
+ r2:
+ provider: r2
+ bucket: ledger-raw
+ key: raw/nz/stats_nz/subnational_population_estimates_2025/2025/001e8a896cfb50f5ed17836dc815b235e3bcca55ee91c9869a2afaeb054b50a6/subnational-population-estimates-at-30-june-2025-provisional.xlsx
+ uri: r2://ledger-raw/raw/nz/stats_nz/subnational_population_estimates_2025/2025/001e8a896cfb50f5ed17836dc815b235e3bcca55ee91c9869a2afaeb054b50a6/subnational-population-estimates-at-30-june-2025-provisional.xlsx
diff --git a/db/data/stats_nz/subnational_population_estimates_2025/subnational-population-estimates-at-30-june-2025-provisional.xlsx b/db/data/stats_nz/subnational_population_estimates_2025/subnational-population-estimates-at-30-june-2025-provisional.xlsx
new file mode 100644
index 0000000..8b551d3
Binary files /dev/null and b/db/data/stats_nz/subnational_population_estimates_2025/subnational-population-estimates-at-30-june-2025-provisional.xlsx differ
diff --git a/docs/pe-nz-population-source-notes.md b/docs/pe-nz-population-source-notes.md
new file mode 100644
index 0000000..a51df0b
--- /dev/null
+++ b/docs/pe-nz-population-source-notes.md
@@ -0,0 +1,98 @@
+# NZ population source notes
+
+This records the bounded source-ingestion work for demographic packages 1 and 2
+of [issue #176](https://github.com/PolicyEngine/chronicle/issues/176), checked on
+30 August 2026 UTC. It does not mark the demographic target surface complete.
+
+## Pinned publisher evidence
+
+| Artifact | Publisher reference | SHA-256 | Bytes |
+|---|---|---|---:|
+| Subnational population workbook | At 30 June 2025, published 29 October 2025 | `001e8a896cfb50f5ed17836dc815b235e3bcca55ee91c9869a2afaeb054b50a6` | 97,990 |
+| Regional council code/name attributes | Boundaries at 1 January 2025 | `e5d53fa6abf742121d123bff970c77658a16e8c9b405b83d1a978cc22920d660` | 3,381 |
+| National population release landing page only | At 30 June 2025, published 19 August 2025 | `b2d2751800857895b1498d87df6f03ac3511ec8c7a9b17cc3150c8ab56533053` | 61,286 |
+
+All artifacts came directly from Stats NZ's website or Stats NZ's official
+geographic service. Their manifests record exact publisher URLs, hashes,
+retrieval timestamps, sizes, and immutable country-scoped R2 keys:
+
+- [Subnational workbook manifest](../db/data/stats_nz/subnational_population_estimates_2025/manifest.yaml).
+- [REGC 2025 classification manifest](../db/data/stats_nz/regional_council_2025_codes/manifest.yaml).
+- [National landing-page evidence manifest](../db/data/stats_nz/national_population_estimates_2025/manifest.yaml).
+
+The objects use
+`r2://ledger-raw/raw/nz/stats_nz//2025//`.
+After upload, all three remote objects were downloaded through the authorized
+Chronicle/Wrangler path and their byte counts and SHA-256 hashes matched the
+local publisher artifacts.
+The two population references share a 30 June 2025 reference date, not a
+publication date. The national landing page is evidence of the missing download
+surface, **not** a single-year-age data artifact or a national source package.
+The classification is geography evidence only and emits no population facts.
+
+## S7: partial workbook package
+
+The [source package](../packages/stats_nz/subnational_population_estimates_2025/README.md)
+admits 85 cells from Tables 1 and 3: 17 geographic rows × all ages/four broad
+age bands. Its precise scope, semantics, and guards are documented there.
+
+The workbook does not contain the requested five-year-age × sex × region cut.
+Stats NZ's public metadata catalog lists `POPES_SUB_004`:
+
+> Subnational population estimates (RC, SA2), by age and sex, at 30 June 1996-2025 (2025 boundaries)
+
+This identifies the needed population/age/sex/boundary family, not a claim that
+the query's original publication vintage has been verified. The documented API
+request for its structure returned **HTTP 401** without a subscription:
+
+```text
+https://api.data.stats.govt.nz/rest/dataflow/STATSNZ/POPES_SUB_004/1.0?references=all&detail=referencepartial
+```
+
+The [official API guide](https://www.stats.govt.nz/tools/aotearoa-data-explorer/ade-api-user-guide/)
+describes API subscription access. No existing subscription was available to
+this ingestion run. No account was created and no authentication bypass was
+attempted. An authorized export still needs verification of the exact five-year
+age categories, sex categories, 2025 REGC boundaries, reference date, and source
+revision vintage before it can extend this package. In particular,
+`POPES_SUB_003` advertises **2026 boundaries** and is not a substitute.
+
+## S8: national single-year ages remain missing
+
+The pinned [19 August 2025 national release](https://www.stats.govt.nz/information-releases/national-population-estimates-at-30-june-2025/)
+contains headline counts and directs users to Infoshare's Population / Population
+Estimates – DPE tables for annual and quarterly single-year-age data. Its page
+payload contains no downloadable XLSX/CSV document block. This corrects the
+inventory's assumption that the release itself provided a single-year-age XLSX.
+
+No reproducible publisher single-year-age artifact for this original reference
+date/publication vintage was acquired. The public ADE catalog query did not
+identify a corresponding national 2025 age table. The missing artifact remains
+an access/discovery gap, not a zero population count. A subscribed API request
+may assist discovery, but access alone does not establish that the required
+original-vintage table is available.
+
+The release explicitly says that Stats NZ revises the preceding six quarters.
+A current 2025 observation may therefore differ from the 19 August 2025
+publication. An eventual publisher export must carry its actual revision
+vintage; it must not be relabeled as the original release. No latest-2026
+population, projection, mirrored third-party bytes, or inferred age allocation
+has been substituted. No national single-year-age source-package alias is
+registered, and the national facts count added by S8 is **zero**.
+
+## Validation and boundary
+
+The partial S7 package passes source-package validation, exact source-cell
+comparisons, geography mapping checks, source-semantic drift tests, consumer
+contract validation, and an explicit one-package build. The bounded source
+build records 85/85 lineaged facts and 2,265 parsed cells.
+
+The source build warns that Axiom CLI canonical-concept validation did not run.
+All mappings are source-label relations; this work claims neither Axiom
+canonical certification nor calibration quality.
+
+Microcosm retains responsibility for choosing constraints, reconciling source
+universes, aging, imputing, constructing solver targets, and activating a
+population. The 85 counts are available source facts, not proof that these
+downstream steps have run. Independent source-fidelity and boundary review is
+still required before landing.
diff --git a/docs/pe-nz-source-checklist.md b/docs/pe-nz-source-checklist.md
index c0b362a..41ef2a2 100644
--- a/docs/pe-nz-source-checklist.md
+++ b/docs/pe-nz-source-checklist.md
@@ -53,8 +53,8 @@ tests, and record a verified `raw/nz/...` R2 URI.
| Package from #176 | Artifact pinned | Package valid | `raw/nz` verified | Notes |
|---|---:|---:|---:|---|
-| `stats_nz/subnational_population_estimates_2025` | [ ] | [ ] | [ ] | |
-| `stats_nz/national_population_estimates_2025` | [ ] | [ ] | [ ] | |
+| `stats_nz/subnational_population_estimates_2025` | [x] | [ ] | [x] | **Partial:** 85 validated workbook counts (16 regions + published NZ total; all ages/four broad age bands). Five-year-age × sex coverage is missing; its ADE structure request returned HTTP 401. Package-completion box stays unchecked. [Evidence and access notes](pe-nz-population-source-notes.md). |
+| `stats_nz/national_population_estimates_2025` | [ ] | [ ] | [ ] | **Blocked:** no original-vintage single-year-age artifact acquired. The 19 August 2025 release points to Infoshare, not an XLSX. Landing-page evidence only is hash-pinned in `raw/nz`; it emits zero facts. [Evidence and access notes](pe-nz-population-source-notes.md). |
| `stats_nz/census_2023_households_by_region` | [ ] | [ ] | [ ] | |
| `stats_nz/census_2023_family_type` | [ ] | [ ] | [ ] | |
| `stats_nz/census_2023_ethnicity_age_region` | [ ] | [ ] | [ ] | |
diff --git a/packages/stats_nz/subnational_population_estimates_2025/README.md b/packages/stats_nz/subnational_population_estimates_2025/README.md
new file mode 100644
index 0000000..07e1f76
--- /dev/null
+++ b/packages/stats_nz/subnational_population_estimates_2025/README.md
@@ -0,0 +1,77 @@
+# Stats NZ subnational population, 30 June 2025 — partial
+
+This package emits 85 publisher-backed population counts: the 16 regional
+council areas and the separately published New Zealand total, each for all ages
+and the four source age bands. It does **not** complete the five-year-age × sex ×
+region surface requested by [issue #176](https://github.com/PolicyEngine/chronicle/issues/176).
+
+The [29 October 2025 release](https://www.stats.govt.nz/information-releases/subnational-population-estimates-at-30-june-2025/)
+and its unchanged XLSX provide these cells:
+
+| Source sheet | Exact selection | Facts |
+|---|---|---:|
+| Table 1 | D7:D22 and D25, headed `2025 P` | 17 |
+| Table 3 | Columns D:G, rows 10, 13, 16, 19, 22, 25, 28, 31, 34, 37, 40, 43, 46, 49, 52, 55, 64 | 68 |
+
+Table 3's columns are 0–14, 15–39, 40–64, and 65+. All selected counts include
+all sexes; no sex-specific counts appear in these worksheets. Both source
+worksheets are parsed over their complete used ranges (2,265 cells), including
+unselected years and explanatory footnotes. The package does not ingest
+territorial authority/local board tables, island subtotals, percentages, median
+ages, or population-change components.
+
+## Reference period and population definition
+
+- Reference date: **30 June 2025**, expressed as `calendar_year: 2025` plus
+ equal start/end coverage dates `2025-06-30`. These are point-in-time counts,
+ not annual average populations.
+- Publication vintage: **29 October 2025**; all selected figures are
+ **provisional**. The source artifact remains pinned even if Stats NZ later
+ revises its historical estimates.
+- Population: estimated resident population (ERP), based on the 2023 census
+ usually resident population and adjusted for net census under/overcount,
+ residents temporarily overseas on census night, and subsequent births,
+ deaths, and net migration. This is not the unadjusted census-night count.
+- Geography: regional boundaries at **1 January 2025**, with REGC codes stored
+ as strings and `geography_vintage: regc_2025`. The separately pinned official
+ [REGC 2025 attributes](https://services2.arcgis.com/vKb0s8tBIA3bdocZ/arcgis/rest/services/Regional_Council_2025/FeatureServer)
+ supply the codes and names. The classification names code `02` “Auckland”;
+ the population workbook labels the same region “Auckland region”.
+- National scope: Table 1!D25 publishes **5,324,700**, including areas outside
+ regions, such as Chatham Islands territory. The package reads the published
+ national cells directly. It neither sums the 16 regions nor constructs a
+ code-`99` outside-region residual. Published rounding also prevents exact
+ reconciliation from being assumed.
+- Provenance: `census` for a census-controlled ERP estimate, and
+ `assertion: observation` for the historical reference date. “Observation”
+ does not mean final or error-free; the source's provisional status remains
+ explicit in the period and concept evidence.
+- Concept relation: `source_label`, with Stats NZ evidence. No Axiom canonical
+ equivalence, population calibration, uprating, interpolation, or target
+ activation is claimed.
+
+Every numeric value is a single selected source cell with scale 1. Row labels,
+age headers, reference years, the boundary footnote, and the provisional-status
+footnote have guards. The tests compare every admitted value against the
+publisher workbook and every regional code/name against the independent
+publisher classification.
+
+## Artifacts and remaining access gaps
+
+See [NZ population source notes](../../../docs/pe-nz-population-source-notes.md)
+for the three pinned checksums, raw storage, the detailed S7 API access failure,
+and S8's missing single-year-age artifact. The wave-1 checklist deliberately
+keeps the package-completion checkbox unchecked.
+
+## Reproduce the bounded checks
+
+```sh
+uv run chronicle validate-package stats-nz-subnational-population-estimates-2025 --year 2025
+uv run chronicle build-suite stats-nz-subnational-population-estimates-2025 --year 2025 --out /tmp/chronicle-nz-population-suite
+uv run pytest -q tests/test_chronicle_nz_population.py tests/test_chronicle_artifacts.py tests/test_source_package_alias_drift.py
+```
+
+Source and consumer checks pass for this partial package. The concept-alignment
+warning records that no Axiom CLI metadata validation ran; the source-label
+mapping is not a certified canonical alignment. Independent source-fidelity
+and boundary judges must review this candidate before landing.
diff --git a/packages/stats_nz/subnational_population_estimates_2025/source_package.yaml b/packages/stats_nz/subnational_population_estimates_2025/source_package.yaml
new file mode 100644
index 0000000..004e381
--- /dev/null
+++ b/packages/stats_nz/subnational_population_estimates_2025/source_package.yaml
@@ -0,0 +1,1066 @@
+# Partial #176 source package: 85 directly published counts, not the missing
+# five-year-age x sex x region table. See README.md for scope and source access.
+# The all-age national total is Table 1!D25, never the sum of the 16 regions.
+# REGC codes/names come from the separately pinned Stats NZ REGC2025 attributes.
+schema_version: ledger.source_package.v1
+package_id: stats-nz-subnational-population-estimates-2025
+label: 'Stats NZ 30 June 2025 provisional regional population: all ages and broad age groups (partial)'
+artifact:
+ source_name: stats_nz
+ source_table: 'Subnational population estimates: At 30 June 2025 (provisional), Tables 1 and 3'
+ resource_package: db
+ resource_directory: data/stats_nz/subnational_population_estimates_2025
+ manifest: manifest.yaml
+ vintage: stats_nz_subnational_population_2025_10_29
+ extracted_at: '2026-08-30'
+ extraction_method: xlsx used-range cell parse restricted to the two source worksheets
+ parser: xlsx_used_range
+ sheets:
+ - Table 1
+ - Table 3
+ artifact_year: 2025
+record_sets:
+- &population_set
+ record_set_id: stats_nz.cy2025.subnational_population.all_ages
+ record_set_spec_id: stats_nz.subnational_population.all_ages.v1
+ source_record_id_prefix: stats_nz.cy2025.subnational_population.all_ages
+ provenance_class: census
+ assertion: observation
+ sheet_name: Table 1
+ period_type: calendar_year
+ period: 2025
+ period_coverage:
+ start_date: '2025-06-30'
+ end_date: '2025-06-30'
+ basis: calendar
+ source_period_label: At 30 June 2025 (provisional)
+ notes: Point-in-time estimated resident population, not an annual average; published 29 October 2025 using the 2023 census base.
+ geography_id: NZ
+ geography_level: country
+ geography_name: New Zealand
+ geography_vintage: current
+ entity: person
+ entity_role: resident_population
+ domain: resident_population
+ groupby_dimension: geography
+ measures:
+ - &population_measure
+ measure_id: population
+ label: Estimated resident population
+ ordinal: 0
+ column: D
+ source_column_id: estimated_resident_population
+ concept: stats_nz.estimated_resident_population
+ source_concept: stats_nz.estimated_resident_population
+ concept_relation: source_label
+ concept_authority: stats_nz
+ concept_evidence_url: https://www.stats.govt.nz/information-releases/subnational-population-estimates-at-30-june-2025/
+ concept_evidence_notes: >-
+ Tables 1 and 3 publish provisional population counts at 30 June 2025,
+ using 1 January 2025 boundaries and the 2023 census base. ERP includes
+ adjustments for net census under/overcount, residents temporarily overseas
+ on census night, and subsequent births, deaths, and net migration.
+ These counts include all sexes; there is no sex-specific split in these
+ worksheets. Only all ages and the source bands 0–14, 15–39, 40–64, and 65+
+ are admitted, not five-year or single-year ages. The New Zealand row
+ includes areas outside regions (for example Chatham Islands territory);
+ it is read directly, never constructed from regional counts. Published
+ rounding means component counts need not sum to totals. Source-only
+ facts: no uprating, interpolation, target activation, or calibration.
+ unit: count
+ aggregation: sum
+ expected_cell_type: number
+ expected_column_header_row: 6
+ expected_column_header: 2025 P
+ rows:
+ - value_id: regc_01
+ label: "Northland region"
+ ordinal: 0
+ row_number: 7
+ expected_row_header_column: A
+ expected_row_header: "Northland region"
+ geography_id: "01"
+ geography_level: region
+ geography_name: "Northland Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 29
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 34
+ expected_value: P provisional
+ label: publication status
+ - value_id: regc_02
+ label: "Auckland region"
+ ordinal: 1
+ row_number: 8
+ expected_row_header_column: A
+ expected_row_header: "Auckland region"
+ geography_id: "02"
+ geography_level: region
+ geography_name: "Auckland"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 29
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 34
+ expected_value: P provisional
+ label: publication status
+ - value_id: regc_03
+ label: "Waikato region"
+ ordinal: 2
+ row_number: 9
+ expected_row_header_column: A
+ expected_row_header: "Waikato region"
+ geography_id: "03"
+ geography_level: region
+ geography_name: "Waikato Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 29
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 34
+ expected_value: P provisional
+ label: publication status
+ - value_id: regc_04
+ label: "Bay of Plenty region"
+ ordinal: 3
+ row_number: 10
+ expected_row_header_column: A
+ expected_row_header: "Bay of Plenty region"
+ geography_id: "04"
+ geography_level: region
+ geography_name: "Bay of Plenty Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 29
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 34
+ expected_value: P provisional
+ label: publication status
+ - value_id: regc_05
+ label: "Gisborne region"
+ ordinal: 4
+ row_number: 11
+ expected_row_header_column: A
+ expected_row_header: "Gisborne region"
+ geography_id: "05"
+ geography_level: region
+ geography_name: "Gisborne Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 29
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 34
+ expected_value: P provisional
+ label: publication status
+ - value_id: regc_06
+ label: "Hawke's Bay region"
+ ordinal: 5
+ row_number: 12
+ expected_row_header_column: A
+ expected_row_header: "Hawke's Bay region"
+ geography_id: "06"
+ geography_level: region
+ geography_name: "Hawke's Bay Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 29
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 34
+ expected_value: P provisional
+ label: publication status
+ - value_id: regc_07
+ label: "Taranaki region"
+ ordinal: 6
+ row_number: 13
+ expected_row_header_column: A
+ expected_row_header: "Taranaki region"
+ geography_id: "07"
+ geography_level: region
+ geography_name: "Taranaki Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 29
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 34
+ expected_value: P provisional
+ label: publication status
+ - value_id: regc_08
+ label: "Manawatū-Whanganui region"
+ ordinal: 7
+ row_number: 14
+ expected_row_header_column: A
+ expected_row_header: "Manawatū-Whanganui region"
+ geography_id: "08"
+ geography_level: region
+ geography_name: "Manawatū-Whanganui Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 29
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 34
+ expected_value: P provisional
+ label: publication status
+ - value_id: regc_09
+ label: "Wellington region"
+ ordinal: 8
+ row_number: 15
+ expected_row_header_column: A
+ expected_row_header: "Wellington region"
+ geography_id: "09"
+ geography_level: region
+ geography_name: "Wellington Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 29
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 34
+ expected_value: P provisional
+ label: publication status
+ - value_id: regc_16
+ label: "Tasman region"
+ ordinal: 9
+ row_number: 16
+ expected_row_header_column: A
+ expected_row_header: "Tasman region"
+ geography_id: "16"
+ geography_level: region
+ geography_name: "Tasman Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 29
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 34
+ expected_value: P provisional
+ label: publication status
+ - value_id: regc_17
+ label: "Nelson region"
+ ordinal: 10
+ row_number: 17
+ expected_row_header_column: A
+ expected_row_header: "Nelson region"
+ geography_id: "17"
+ geography_level: region
+ geography_name: "Nelson Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 29
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 34
+ expected_value: P provisional
+ label: publication status
+ - value_id: regc_18
+ label: "Marlborough region"
+ ordinal: 11
+ row_number: 18
+ expected_row_header_column: A
+ expected_row_header: "Marlborough region"
+ geography_id: "18"
+ geography_level: region
+ geography_name: "Marlborough Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 29
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 34
+ expected_value: P provisional
+ label: publication status
+ - value_id: regc_12
+ label: "West Coast region"
+ ordinal: 12
+ row_number: 19
+ expected_row_header_column: A
+ expected_row_header: "West Coast region"
+ geography_id: "12"
+ geography_level: region
+ geography_name: "West Coast Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 29
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 34
+ expected_value: P provisional
+ label: publication status
+ - value_id: regc_13
+ label: "Canterbury region"
+ ordinal: 13
+ row_number: 20
+ expected_row_header_column: A
+ expected_row_header: "Canterbury region"
+ geography_id: "13"
+ geography_level: region
+ geography_name: "Canterbury Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 29
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 34
+ expected_value: P provisional
+ label: publication status
+ - value_id: regc_14
+ label: "Otago region"
+ ordinal: 14
+ row_number: 21
+ expected_row_header_column: A
+ expected_row_header: "Otago region"
+ geography_id: "14"
+ geography_level: region
+ geography_name: "Otago Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 29
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 34
+ expected_value: P provisional
+ label: publication status
+ - value_id: regc_15
+ label: "Southland region"
+ ordinal: 15
+ row_number: 22
+ expected_row_header_column: A
+ expected_row_header: "Southland region"
+ geography_id: "15"
+ geography_level: region
+ geography_name: "Southland Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 29
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 34
+ expected_value: P provisional
+ label: publication status
+ - value_id: nz
+ label: "New Zealand"
+ ordinal: 16
+ row_number: 25
+ expected_row_header_column: A
+ expected_row_header: "New Zealand(3)"
+ geography_id: "NZ"
+ geography_level: country
+ geography_name: "New Zealand"
+ geography_vintage: current
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 29
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 34
+ expected_value: P provisional
+ label: publication status
+- <<: *population_set
+ record_set_id: stats_nz.cy2025.subnational_population.age_0_14
+ provenance_class: census
+ record_set_spec_id: stats_nz.subnational_population.age_0_14.v1
+ source_record_id_prefix: stats_nz.cy2025.subnational_population.age_0_14
+ sheet_name: Table 3
+ shared_filters:
+ person.age_band: "0_14"
+ shared_constraints:
+ - variable: person.age_band
+ operator: ==
+ value: "0_14"
+ label: Publisher broad age group
+ - variable: person.age
+ operator: '>='
+ value: 0
+ unit: years
+ label: Age lower bound
+ - variable: person.age
+ operator: <
+ value: 15
+ unit: years
+ label: Age upper bound (exclusive)
+ measures:
+ - <<: *population_measure
+ column: D
+ expected_column_header: "0–14"
+ rows: &broad_age_rows
+ - value_id: regc_01
+ label: "Northland region"
+ ordinal: 0
+ row_number: 10
+ expected_row_header_column: A
+ expected_row_header: "Northland region"
+ geography_id: "01"
+ geography_level: region
+ geography_name: "Northland Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 68
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 74
+ expected_value: P provisional
+ label: publication status
+ - column: C
+ row: 10
+ expected_value: 2025 P
+ label: historical 2025 reference-date row
+ - value_id: regc_02
+ label: "Auckland region"
+ ordinal: 1
+ row_number: 13
+ expected_row_header_column: A
+ expected_row_header: "Auckland region"
+ geography_id: "02"
+ geography_level: region
+ geography_name: "Auckland"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 68
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 74
+ expected_value: P provisional
+ label: publication status
+ - column: C
+ row: 13
+ expected_value: 2025 P
+ label: historical 2025 reference-date row
+ - value_id: regc_03
+ label: "Waikato region"
+ ordinal: 2
+ row_number: 16
+ expected_row_header_column: A
+ expected_row_header: "Waikato region"
+ geography_id: "03"
+ geography_level: region
+ geography_name: "Waikato Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 68
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 74
+ expected_value: P provisional
+ label: publication status
+ - column: C
+ row: 16
+ expected_value: 2025 P
+ label: historical 2025 reference-date row
+ - value_id: regc_04
+ label: "Bay of Plenty region"
+ ordinal: 3
+ row_number: 19
+ expected_row_header_column: A
+ expected_row_header: "Bay of Plenty region"
+ geography_id: "04"
+ geography_level: region
+ geography_name: "Bay of Plenty Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 68
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 74
+ expected_value: P provisional
+ label: publication status
+ - column: C
+ row: 19
+ expected_value: 2025 P
+ label: historical 2025 reference-date row
+ - value_id: regc_05
+ label: "Gisborne region"
+ ordinal: 4
+ row_number: 22
+ expected_row_header_column: A
+ expected_row_header: "Gisborne region"
+ geography_id: "05"
+ geography_level: region
+ geography_name: "Gisborne Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 68
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 74
+ expected_value: P provisional
+ label: publication status
+ - column: C
+ row: 22
+ expected_value: 2025 P
+ label: historical 2025 reference-date row
+ - value_id: regc_06
+ label: "Hawke's Bay region"
+ ordinal: 5
+ row_number: 25
+ expected_row_header_column: A
+ expected_row_header: "Hawke's Bay region"
+ geography_id: "06"
+ geography_level: region
+ geography_name: "Hawke's Bay Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 68
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 74
+ expected_value: P provisional
+ label: publication status
+ - column: C
+ row: 25
+ expected_value: 2025 P
+ label: historical 2025 reference-date row
+ - value_id: regc_07
+ label: "Taranaki region"
+ ordinal: 6
+ row_number: 28
+ expected_row_header_column: A
+ expected_row_header: "Taranaki region"
+ geography_id: "07"
+ geography_level: region
+ geography_name: "Taranaki Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 68
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 74
+ expected_value: P provisional
+ label: publication status
+ - column: C
+ row: 28
+ expected_value: 2025 P
+ label: historical 2025 reference-date row
+ - value_id: regc_08
+ label: "Manawatū-Whanganui region"
+ ordinal: 7
+ row_number: 31
+ expected_row_header_column: A
+ expected_row_header: "Manawatū-Whanganui region"
+ geography_id: "08"
+ geography_level: region
+ geography_name: "Manawatū-Whanganui Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 68
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 74
+ expected_value: P provisional
+ label: publication status
+ - column: C
+ row: 31
+ expected_value: 2025 P
+ label: historical 2025 reference-date row
+ - value_id: regc_09
+ label: "Wellington region"
+ ordinal: 8
+ row_number: 34
+ expected_row_header_column: A
+ expected_row_header: "Wellington region"
+ geography_id: "09"
+ geography_level: region
+ geography_name: "Wellington Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 68
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 74
+ expected_value: P provisional
+ label: publication status
+ - column: C
+ row: 34
+ expected_value: 2025 P
+ label: historical 2025 reference-date row
+ - value_id: regc_16
+ label: "Tasman region"
+ ordinal: 9
+ row_number: 37
+ expected_row_header_column: A
+ expected_row_header: "Tasman region"
+ geography_id: "16"
+ geography_level: region
+ geography_name: "Tasman Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 68
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 74
+ expected_value: P provisional
+ label: publication status
+ - column: C
+ row: 37
+ expected_value: 2025 P
+ label: historical 2025 reference-date row
+ - value_id: regc_17
+ label: "Nelson region"
+ ordinal: 10
+ row_number: 40
+ expected_row_header_column: A
+ expected_row_header: "Nelson region"
+ geography_id: "17"
+ geography_level: region
+ geography_name: "Nelson Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 68
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 74
+ expected_value: P provisional
+ label: publication status
+ - column: C
+ row: 40
+ expected_value: 2025 P
+ label: historical 2025 reference-date row
+ - value_id: regc_18
+ label: "Marlborough region"
+ ordinal: 11
+ row_number: 43
+ expected_row_header_column: A
+ expected_row_header: "Marlborough region"
+ geography_id: "18"
+ geography_level: region
+ geography_name: "Marlborough Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 68
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 74
+ expected_value: P provisional
+ label: publication status
+ - column: C
+ row: 43
+ expected_value: 2025 P
+ label: historical 2025 reference-date row
+ - value_id: regc_12
+ label: "West Coast region"
+ ordinal: 12
+ row_number: 46
+ expected_row_header_column: A
+ expected_row_header: "West Coast region"
+ geography_id: "12"
+ geography_level: region
+ geography_name: "West Coast Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 68
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 74
+ expected_value: P provisional
+ label: publication status
+ - column: C
+ row: 46
+ expected_value: 2025 P
+ label: historical 2025 reference-date row
+ - value_id: regc_13
+ label: "Canterbury region"
+ ordinal: 13
+ row_number: 49
+ expected_row_header_column: A
+ expected_row_header: "Canterbury region"
+ geography_id: "13"
+ geography_level: region
+ geography_name: "Canterbury Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 68
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 74
+ expected_value: P provisional
+ label: publication status
+ - column: C
+ row: 49
+ expected_value: 2025 P
+ label: historical 2025 reference-date row
+ - value_id: regc_14
+ label: "Otago region"
+ ordinal: 14
+ row_number: 52
+ expected_row_header_column: A
+ expected_row_header: "Otago region"
+ geography_id: "14"
+ geography_level: region
+ geography_name: "Otago Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 68
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 74
+ expected_value: P provisional
+ label: publication status
+ - column: C
+ row: 52
+ expected_value: 2025 P
+ label: historical 2025 reference-date row
+ - value_id: regc_15
+ label: "Southland region"
+ ordinal: 15
+ row_number: 55
+ expected_row_header_column: A
+ expected_row_header: "Southland region"
+ geography_id: "15"
+ geography_level: region
+ geography_name: "Southland Region"
+ geography_vintage: regc_2025
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 68
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 74
+ expected_value: P provisional
+ label: publication status
+ - column: C
+ row: 55
+ expected_value: 2025 P
+ label: historical 2025 reference-date row
+ - value_id: nz
+ label: "New Zealand"
+ ordinal: 16
+ row_number: 64
+ expected_row_header_column: A
+ expected_row_header: "New Zealand(4)"
+ geography_id: "NZ"
+ geography_level: country
+ geography_name: "New Zealand"
+ geography_vintage: current
+ table_record_kind: total
+ guard_cells:
+ - column: A
+ row: 4
+ expected_value: 'At 30 June 2023–2025'
+ label: point-in-time release period
+ - column: A
+ row: 68
+ expected_value: '2. Boundaries at 1 January 2025.'
+ label: regional boundary vintage
+ - column: A
+ row: 74
+ expected_value: P provisional
+ label: publication status
+ - column: C
+ row: 64
+ expected_value: 2025 P
+ label: historical 2025 reference-date row
+- <<: *population_set
+ record_set_id: stats_nz.cy2025.subnational_population.age_15_39
+ provenance_class: census
+ record_set_spec_id: stats_nz.subnational_population.age_15_39.v1
+ source_record_id_prefix: stats_nz.cy2025.subnational_population.age_15_39
+ sheet_name: Table 3
+ shared_filters:
+ person.age_band: "15_39"
+ shared_constraints:
+ - variable: person.age_band
+ operator: ==
+ value: "15_39"
+ label: Publisher broad age group
+ - variable: person.age
+ operator: '>='
+ value: 15
+ unit: years
+ label: Age lower bound
+ - variable: person.age
+ operator: <
+ value: 40
+ unit: years
+ label: Age upper bound (exclusive)
+ measures:
+ - <<: *population_measure
+ column: E
+ expected_column_header: "15–39"
+ rows: *broad_age_rows
+- <<: *population_set
+ record_set_id: stats_nz.cy2025.subnational_population.age_40_64
+ provenance_class: census
+ record_set_spec_id: stats_nz.subnational_population.age_40_64.v1
+ source_record_id_prefix: stats_nz.cy2025.subnational_population.age_40_64
+ sheet_name: Table 3
+ shared_filters:
+ person.age_band: "40_64"
+ shared_constraints:
+ - variable: person.age_band
+ operator: ==
+ value: "40_64"
+ label: Publisher broad age group
+ - variable: person.age
+ operator: '>='
+ value: 40
+ unit: years
+ label: Age lower bound
+ - variable: person.age
+ operator: <
+ value: 65
+ unit: years
+ label: Age upper bound (exclusive)
+ measures:
+ - <<: *population_measure
+ column: F
+ expected_column_header: "40–64"
+ rows: *broad_age_rows
+- <<: *population_set
+ record_set_id: stats_nz.cy2025.subnational_population.age_65_plus
+ provenance_class: census
+ record_set_spec_id: stats_nz.subnational_population.age_65_plus.v1
+ source_record_id_prefix: stats_nz.cy2025.subnational_population.age_65_plus
+ sheet_name: Table 3
+ shared_filters:
+ person.age_band: "65_plus"
+ shared_constraints:
+ - variable: person.age_band
+ operator: ==
+ value: "65_plus"
+ label: Publisher broad age group
+ - variable: person.age
+ operator: '>='
+ value: 65
+ unit: years
+ label: Age lower bound
+ measures:
+ - <<: *population_measure
+ column: G
+ expected_column_header: "65+"
+ rows: *broad_age_rows
diff --git a/tests/test_chronicle_bundle.py b/tests/test_chronicle_bundle.py
index 6a00ef8..d6ea0e9 100644
--- a/tests/test_chronicle_bundle.py
+++ b/tests/test_chronicle_bundle.py
@@ -73,16 +73,16 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path):
"aggregate_duplicate_key_count": 0,
"entity_count": 12,
"error_count": 0,
- "fact_count": 171855,
- "geography_count": 12536,
+ "fact_count": 171940,
+ "geography_count": 12553,
"period_count": 192,
"semantic_duplicate_key_count": 121,
"skipped_source_count": 10,
- "source_count": 43,
- "source_package_count": 151,
+ "source_count": 44,
+ "source_package_count": 152,
"warning_count": 1,
}
- assert len(rows) == 171855
+ assert len(rows) == 171940
assert {row["provenance_class"] for row in rows} <= {
"administrative",
"census",
@@ -100,7 +100,7 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path):
)
assert rows[0]["aggregate_fact_key"].startswith("ledger.aggregate_fact.v2:")
assert rows[0]["semantic_fact_key"].startswith("ledger.semantic_fact.v2:")
- assert source_packages["source_package_count"] == 151
+ assert source_packages["source_package_count"] == 152
assert source_packages["skipped_source_count"] == 10
assert sorted(item["source"] for item in source_packages["skipped_sources"]) == [
"census-acs-s0101-congressional-district-age-2024",
@@ -114,7 +114,7 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path):
"jct-obbba-revenue-estimates-2025",
"jct-tax-expenditures-2024",
]
- assert coverage["fact_count"] == 171855
+ assert coverage["fact_count"] == 171940
assert coverage["counts"]["by_source"] == {
"bea": 445,
"bfp_economic_outlook": 5,
@@ -156,12 +156,20 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path):
"statbel_fiscal_income": 565,
"statbel_fiscal_income_distribution": 14600,
"statbel_population_structure": 36,
+ "stats_nz": 85,
"usda_snap": 852,
"voa": 3001,
"welshgov": 216,
}
table_counts = coverage["counts"]["by_source_table"]
- assert len(table_counts) == 146
+ assert len(table_counts) == 147
+ assert (
+ table_counts[
+ "stats_nz:Subnational population estimates: At 30 June 2025 "
+ "(provisional), Tables 1 and 3"
+ ]
+ == 85
+ )
assert (
table_counts[
"dwp:Universal Credit childcare element statistics to August 2025, Table 1"
@@ -533,7 +541,7 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path):
"calendar_year:2022": 2075,
"calendar_year:2023": 6343,
"calendar_year:2024": 33936,
- "calendar_year:2025": 4571,
+ "calendar_year:2025": 4656,
"calendar_year:2026": 341,
"calendar_year:2027": 320,
"calendar_year:2028": 320,
@@ -689,7 +697,10 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path):
)
assert coverage["counts"]["by_geography"]["country:K02000001"] == 4297
assert coverage["counts"]["by_geography"]["country:K03000001"] == 497
- assert len(coverage["counts"]["by_geography"]) == 12536
+ assert coverage["counts"]["by_geography"]["country:NZ"] == 5
+ assert coverage["counts"]["by_geography"]["region:01"] == 5
+ assert coverage["counts"]["by_geography"]["region:02"] == 5
+ assert len(coverage["counts"]["by_geography"]) == 12553
assert coverage["counts"]["by_entity"] == {
"benefit_unit": 233,
"dwelling": 12733,
@@ -699,7 +710,7 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path):
"household": 40724,
"institutional_sector": 133,
"pension_plan": 2,
- "person": 60466,
+ "person": 60551,
"return": 14600,
"social_protection_scheme": 36,
"tax_unit": 40069,
diff --git a/tests/test_chronicle_nz_population.py b/tests/test_chronicle_nz_population.py
new file mode 100644
index 0000000..a6a5f5f
--- /dev/null
+++ b/tests/test_chronicle_nz_population.py
@@ -0,0 +1,322 @@
+"""Source-only regression gates for the partial 30 June 2025 NZ population package."""
+
+from __future__ import annotations
+
+from collections import Counter
+from dataclasses import replace
+import hashlib
+import html
+from io import BytesIO
+import json
+from pathlib import Path
+
+import openpyxl
+import pytest
+import yaml
+
+from chronicle.bundle import build_bundle, build_bundle_coverage
+from chronicle.consumer_contract import (
+ consumer_fact_rows,
+ validate_consumer_fact_contract,
+)
+from chronicle.core import validate_facts
+from chronicle.source_package import (
+ SOURCE_PACKAGE_ALIASES,
+ load_source_package,
+ validate_source_package,
+)
+from chronicle.sources.cells import build_source_cell_key, validate_source_cells
+
+ALIAS = "stats-nz-subnational-population-estimates-2025"
+REPO_ROOT = Path(__file__).resolve().parents[1]
+DATA_ROOT = REPO_ROOT / "db" / "data" / "stats_nz"
+WORKBOOK_SHA256 = "001e8a896cfb50f5ed17836dc815b235e3bcca55ee91c9869a2afaeb054b50a6"
+REGC_SHA256 = "e5d53fa6abf742121d123bff970c77658a16e8c9b405b83d1a978cc22920d660"
+NATIONAL_PAGE_SHA256 = (
+ "b2d2751800857895b1498d87df6f03ac3511ec8c7a9b17cc3150c8ab56533053"
+)
+# Order and rows are from Tables 1 and 3 of the pinned publisher workbook.
+REGIONS = (
+ ("01", 7, 10),
+ ("02", 8, 13),
+ ("03", 9, 16),
+ ("04", 10, 19),
+ ("05", 11, 22),
+ ("06", 12, 25),
+ ("07", 13, 28),
+ ("08", 14, 31),
+ ("09", 15, 34),
+ ("16", 16, 37),
+ ("17", 17, 40),
+ ("18", 18, 43),
+ ("12", 19, 46),
+ ("13", 20, 49),
+ ("14", 21, 52),
+ ("15", 22, 55),
+ ("NZ", 25, 64),
+)
+# These are the source's four broad bands, not five-year-age target bands.
+AGE_BANDS = {
+ "0_14": ("D", 0, 15),
+ "15_39": ("E", 15, 40),
+ "40_64": ("F", 40, 65),
+ "65_plus": ("G", 65, None),
+}
+
+
+def _artifact(package_name):
+ directory = DATA_ROOT / package_name
+ manifest = yaml.safe_load((directory / "manifest.yaml").read_text())
+ entry = manifest["files"][2025]
+ return (directory / entry["filename"]).read_bytes(), entry
+
+
+@pytest.fixture(scope="module")
+def population():
+ package = load_source_package(ALIAS)
+ cells = package.build_source_cells(2025)
+ facts = package.build_facts(2025, cells=cells)
+ return package, cells, facts
+
+
+@pytest.fixture(scope="module")
+def workbook():
+ content, _ = _artifact("subnational_population_estimates_2025")
+ book = openpyxl.load_workbook(BytesIO(content), data_only=True)
+ yield book
+ book.close()
+
+
+@pytest.mark.parametrize(
+ ("package_name", "expected_sha", "expected_size"),
+ [
+ ("subnational_population_estimates_2025", WORKBOOK_SHA256, 97990),
+ ("regional_council_2025_codes", REGC_SHA256, 3381),
+ ("national_population_estimates_2025", NATIONAL_PAGE_SHA256, 61286),
+ ],
+)
+def test_publisher_artifact_bytes_and_country_storage_are_pinned(
+ package_name, expected_sha, expected_size
+):
+ content, entry = _artifact(package_name)
+ assert hashlib.sha256(content).hexdigest() == entry["sha256"] == expected_sha
+ assert len(content) == entry["size_bytes"] == expected_size
+ assert entry["storage"]["r2"]["uri"].startswith(
+ f"r2://ledger-raw/raw/nz/stats_nz/{package_name}/2025/{expected_sha}/"
+ )
+
+
+def test_partial_package_counts_and_whole_sheet_preservation(population):
+ package, cells, facts = population
+ report = validate_source_package(ALIAS, year=2025)
+ assert report.valid
+ assert not report.warnings
+ assert report.counts["record_set_count"] == 5
+ assert report.counts["source_record_count"] == len(facts) == 85
+ assert "partial" in package.label.lower()
+ assert package.artifact.sheets == ("Table 1", "Table 3")
+ # Full used ranges, including unused years and source footnotes: 35x9 + 75x26.
+ assert len(cells) == 2265
+ assert Counter(cell.sheet_name for cell in cells) == {
+ "Table 1": 315,
+ "Table 3": 1950,
+ }
+ assert validate_source_cells(cells).valid
+ assert validate_facts(facts).valid
+
+
+def test_every_fact_equals_one_published_cell_with_no_scaling(population, workbook):
+ package, cells, facts = population
+ actual = {
+ (fact.geography.id, fact.filters.get("person.age_band", "all")): fact
+ for fact in facts
+ }
+ cell_keys = {build_source_cell_key(cell) for cell in cells}
+ assert len(actual) == 85
+ for geography_id, total_row, broad_age_row in REGIONS:
+ assert (
+ actual[(geography_id, "all")].value
+ == workbook["Table 1"][f"D{total_row}"].value
+ )
+ for age_band, (column, _lower, _upper) in AGE_BANDS.items():
+ assert (
+ actual[(geography_id, age_band)].value
+ == workbook["Table 3"][f"{column}{broad_age_row}"].value
+ )
+ for fact, spec in zip(facts, package.build_source_record_specs(2025), strict=True):
+ assert spec.value_scale == 1
+ assert spec.divisor_selector is None
+ assert spec.round_to is None
+ assert spec.selector.end_address is None
+ assert fact.source_record_id == spec.source_record_id
+ assert fact.source_cell_keys
+ assert set(fact.source_cell_keys) <= cell_keys
+ assert fact.source.source_sha256 == WORKBOOK_SHA256
+ assert fact.aggregation.method == "sum"
+ assert fact.measure.unit == "count"
+ assert type(fact.value) is int
+
+
+def test_regc_codes_and_names_match_official_2025_classification(population):
+ _package, _cells, facts = population
+ content, _ = _artifact("regional_council_2025_codes")
+ classification = json.loads(content)
+ assert not classification.get("exceededTransferLimit", False)
+ codes = {
+ row["attributes"]["REGC2025_V1_00"]: row["attributes"]["REGC2025_V1_00_NAME"]
+ for row in classification["features"]
+ }
+ assert len(codes) == 17 # Sixteen regions plus publisher code 99 (outside region).
+ regional_facts = [fact for fact in facts if fact.geography.level == "region"]
+ assert {fact.geography.id for fact in regional_facts} == codes.keys() - {"99"}
+ assert len(regional_facts) == 80
+ assert all(fact.geography.vintage == "regc_2025" for fact in regional_facts)
+ assert all(
+ fact.geography.name == codes[fact.geography.id] for fact in regional_facts
+ )
+ assert (
+ codes["02"] == "Auckland"
+ ) # Workbook calls the same region "Auckland region".
+
+
+def test_national_total_is_published_not_reconciled_from_regions(population, workbook):
+ _package, _cells, facts = population
+ totals = {fact.geography.id: fact for fact in facts if not fact.filters}
+ # Table 1 footnote 3 includes areas outside a region (for example Chatham).
+ assert totals["NZ"].value == workbook["Table 1"]["D25"].value == 5_324_700
+ assert totals["NZ"].value != sum(
+ fact.value for code, fact in totals.items() if code != "NZ"
+ )
+ assert "99" not in totals # Do not construct an outside-region residual.
+ assert totals["NZ"].geography.level == "country"
+ assert totals["NZ"].source_record_id.endswith(".all_ages.nz.population")
+
+
+def test_age_universes_are_only_published_broad_bands_and_all_sexes(population):
+ _package, _cells, facts = population
+ assert {fact.filters.get("person.age_band", "all") for fact in facts} == {
+ "all",
+ *AGE_BANDS,
+ }
+ for fact in facts:
+ assert "person.sex" not in fact.filters
+ assert not any(
+ constraint.variable == "person.sex" for constraint in fact.constraints
+ )
+ assert fact.entity.name == "person"
+ assert fact.entity.role == "resident_population"
+ if not fact.filters:
+ assert not fact.constraints
+ continue
+ _column, lower, upper = AGE_BANDS[fact.filters["person.age_band"]]
+ age_constraints = {
+ (item.operator, item.value, item.unit)
+ for item in fact.constraints
+ if item.variable == "person.age"
+ }
+ expected = {(">=", lower, "years")}
+ if upper is not None:
+ expected.add(("<", upper, "years"))
+ assert age_constraints == expected
+
+
+@pytest.mark.parametrize("requested_year", [2023, 2025, 2026])
+def test_requested_build_year_cannot_uprate_or_relabel_pinned_vintage(
+ population, requested_year
+):
+ package, cells, original = population
+ facts = package.build_facts(requested_year, cells=cells)
+ assert facts == original
+ assert package.artifact.artifact_year == 2025
+ for fact in facts:
+ assert fact.period.type == "calendar_year"
+ assert fact.period.value == 2025
+ assert fact.period_coverage.start_date == "2025-06-30"
+ assert fact.period_coverage.end_date == "2025-06-30"
+ assert fact.period_coverage.basis == "calendar"
+ assert (
+ fact.period_coverage.source_period_label == "At 30 June 2025 (provisional)"
+ )
+ assert fact.source.vintage == "stats_nz_subnational_population_2025_10_29"
+ assert fact.assertion == "observation"
+ assert fact.provenance_class == "census"
+
+
+@pytest.mark.parametrize(
+ ("sheet", "address", "replacement"),
+ [
+ ("Table 1", "D6", "2026 P"),
+ ("Table 1", "A7", "Auckland region"),
+ ("Table 1", "A4", "Year ended 30 June 2023–2025"),
+ ("Table 1", "A29", "2. Boundaries at 1 January 2026."),
+ ("Table 1", "A34", "F final"),
+ ("Table 3", "C13", "2024 P"),
+ ("Table 3", "A13", "Northland region"),
+ ("Table 3", "E6", "15–64"),
+ ("Table 3", "G6", "60+"),
+ ("Table 3", "A68", "2. Boundaries at 1 January 2026."),
+ ("Table 3", "A74", "F final"),
+ ],
+)
+def test_source_semantic_drift_fails_closed(population, sheet, address, replacement):
+ package, cells, _facts = population
+ mutated = [
+ replace(cell, raw_value=replacement, display_value=replacement)
+ if (cell.sheet_name, cell.address) == (sheet, address)
+ else cell
+ for cell in cells
+ ]
+ with pytest.raises(ValueError, match="expected"):
+ package.build_facts(2025, cells=mutated)
+
+
+def test_consumer_rows_preserve_source_only_scope_and_lineage(population):
+ _package, _cells, facts = population
+ rows = consumer_fact_rows(facts)
+ assert validate_consumer_fact_contract(facts).valid
+ assert len({row["aggregate_fact_key"] for row in rows}) == 85
+ assert len({row["semantic_fact_key"] for row in rows}) == 85
+ for row in rows:
+ assert row["concept_alignment"]["relation"] == "source_label"
+ notes = row["concept_alignment"]["evidence_notes"]
+ assert "not five-year or single-year ages" in notes
+ assert "there is no sex-specific split" in notes
+ assert "outside regions" in notes
+ assert row["source"]["raw_r2_key"].startswith("raw/nz/stats_nz/")
+ assert row["lineage"]["source_cell_keys"]
+ assert row["lineage"]["source_record_id"]
+ assert (
+ not {"target", "solver", "calibration", "uprating_factor", "weight"}
+ & row.keys()
+ )
+
+
+def test_national_landing_page_is_evidence_not_an_age_table_package():
+ content, _ = _artifact("national_population_estimates_2025")
+ page = html.unescape(content.decode()).replace("\\/", "/")
+ assert "single-year of age" in page
+ assert "Infoshare" in page
+ assert "2025-08-19 10:45:00" in page
+ assert "DocumentBlock" not in page
+ assert "stats-nz-national-population-estimates-2025" not in SOURCE_PACKAGE_ALIASES
+ assert not (
+ REPO_ROOT
+ / "packages/stats_nz/national_population_estimates_2025/source_package.yaml"
+ ).exists()
+
+
+def test_explicit_single_package_bundle_passes_all_source_gates(tmp_path):
+ report = build_bundle(tmp_path / "bundle", sources=[ALIAS], year=2025)
+ assert report.valid
+ rows = [
+ json.loads(line)
+ for line in (tmp_path / "bundle/consumer_facts.jsonl").read_text().splitlines()
+ ]
+ coverage = build_bundle_coverage(rows)
+ assert coverage["fact_count"] == 85
+ assert coverage["counts"]["by_source"] == {"stats_nz": 85}
+ assert coverage["counts"]["by_period"] == {"calendar_year:2025": 85}
+ assert coverage["counts"]["by_geography"]["country:NZ"] == 5
+ assert len(coverage["counts"]["by_geography"]) == 17
+ assert not coverage["duplicates"]["aggregate_fact_keys"]
+ assert not coverage["duplicates"]["semantic_fact_keys"]