diff --git a/.Rbuildignore b/.Rbuildignore
index 4510f3b..9df34dc 100644
--- a/.Rbuildignore
+++ b/.Rbuildignore
@@ -16,4 +16,13 @@ paper.pdf$
^\.github$
^codecov\.yml$
^README\.Rmd$
+^README\.html$
^CRAN-SUBMISSION$
+^data-raw$
+^AUDIT\.md$
+^CLAUDE\.md$
+^issue_comments$
+^bench$
+^ROADMAP\.md$
+^ISSUE-RESOLUTION\.md$
+^vignettes/.*\.html$
diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml
index fb7b370..fefd979 100644
--- a/.github/workflows/R-CMD-check.yaml
+++ b/.github/workflows/R-CMD-check.yaml
@@ -1,17 +1,14 @@
-# For help debugging build failures open an issue on the RStudio community with the 'github-actions' tag.
-# https://community.rstudio.com/new-topic?category=Package%20development&tags=github-actions
+# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
on:
push:
- branches:
- - main
- - master
+ branches: [main, master]
pull_request:
- branches:
- - main
- - master
+ branches: [main, master]
name: R-CMD-check
+permissions: read-all
+
jobs:
R-CMD-check:
runs-on: ${{ matrix.config.os }}
@@ -22,65 +19,41 @@ jobs:
fail-fast: false
matrix:
config:
+ - {os: macos-latest, r: 'release'}
- {os: windows-latest, r: 'release'}
- - {os: macOS-latest, r: 'release'}
- - {os: ubuntu-20.04, r: 'release', rspm: "https://packagemanager.rstudio.com/cran/__linux__/focal/latest"}
- - {os: ubuntu-20.04, r: 'devel', rspm: "https://packagemanager.rstudio.com/cran/__linux__/focal/latest"}
+ - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'}
+ - {os: ubuntu-latest, r: 'release'}
+ - {os: ubuntu-latest, r: 'oldrel-1'}
env:
- R_REMOTES_NO_ERRORS_FROM_WARNINGS: true
- RSPM: ${{ matrix.config.rspm }}
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
+ R_KEEP_PKG_SOURCE: yes
steps:
- - uses: actions/checkout@v2
-
- - uses: r-lib/actions/setup-r@v1
+ - uses: actions/checkout@v4
with:
- r-version: ${{ matrix.config.r }}
-
- - uses: r-lib/actions/setup-pandoc@v1
+ fetch-depth: 0
- - name: Query dependencies
- run: |
- install.packages('remotes')
- saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2)
- writeLines(sprintf("R-%i.%i", getRversion()$major, getRversion()$minor), ".github/R-version")
- shell: Rscript {0}
+ - uses: r-lib/actions/setup-pandoc@v2
- - name: Restore R package cache
- if: runner.os != 'Windows'
- uses: actions/cache@v2
+ - uses: r-lib/actions/setup-r@v2
with:
- path: ${{ env.R_LIBS_USER }}
- key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-${{ hashFiles('.github/depends.Rds') }}
- restore-keys: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-
-
- - name: Install system dependencies
- if: runner.os == 'Linux'
- run: |
- while read -r cmd
- do
- eval sudo $cmd
- done < <(Rscript -e 'writeLines(remotes::system_requirements("ubuntu", "20.04"))')
+ r-version: ${{ matrix.config.r }}
+ http-user-agent: ${{ matrix.config.http-user-agent }}
+ use-public-rspm: true
- - name: Install dependencies
- run: |
- remotes::install_deps(dependencies = TRUE)
- remotes::install_cran("rcmdcheck")
- shell: Rscript {0}
+ - uses: r-lib/actions/setup-r-dependencies@v2
+ with:
+ extra-packages: any::rcmdcheck
+ needs: check
- - name: Check
+ - name: Exact zipcodeR 0.3.5 differential gate
env:
- _R_CHECK_CRAN_INCOMING_REMOTE_: false
- run: |
- options(crayon.enabled = TRUE)
- rcmdcheck::rcmdcheck(args = c("--no-manual", "--as-cran"), error_on = "warning", check_dir = "check")
- shell: Rscript {0}
+ ZIPCODER_BASELINE_REF: origin/master
+ run: Rscript tools/compatibility-check.R
- - name: Upload check results
- if: failure()
- uses: actions/upload-artifact@main
+ - uses: r-lib/actions/check-r-package@v2
with:
- name: ${{ runner.os }}-r${{ matrix.config.r }}-results
- path: check
+ args: 'c("--no-manual", "--as-cran")'
+ upload-snapshots: true
+ build_args: 'c("--no-manual", "--compact-vignettes=gs+qpdf")'
diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml
index c31e289..6e42152 100644
--- a/.github/workflows/pkgdown.yaml
+++ b/.github/workflows/pkgdown.yaml
@@ -32,13 +32,9 @@ jobs:
- uses: r-lib/actions/setup-r-dependencies@v2
with:
- extra-packages: any::pkgdown, local::.
+ # the preferably pkgdown template is no longer on CRAN; install from GitHub
+ extra-packages: any::pkgdown, github::amirmasoudabdol/preferably, local::.
needs: website
-
- - name: Install dependencies
- run: |
- remotes::install_deps(dependencies = TRUE)
- install.packages(c("pkgdown","preferably"), type = "binary")
- name: Build site
run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE)
diff --git a/.github/workflows/refresh-data.yaml b/.github/workflows/refresh-data.yaml
new file mode 100644
index 0000000..53178d0
--- /dev/null
+++ b/.github/workflows/refresh-data.yaml
@@ -0,0 +1,84 @@
+name: data-assets
+
+on:
+ workflow_dispatch:
+ inputs:
+ mode:
+ description: Inspect upstream sources or rebuild an already-pinned release
+ required: true
+ type: choice
+ options: [refresh, rebuild]
+ data_version:
+ description: Explicit immutable data version (for example 2026.08)
+ required: true
+ type: string
+ build_timestamp:
+ description: Explicit ISO-8601 build timestamp
+ required: true
+ type: string
+
+permissions:
+ contents: read
+
+jobs:
+ data-assets:
+ runs-on: ubuntu-latest
+ env:
+ CENSUS_API_KEY: ${{ secrets.CENSUS_API_KEY }}
+ PIPELINE_DATA_VERSION: ${{ inputs.data_version }}
+ PIPELINE_PROPOSED_VERSION: ${{ inputs.data_version }}
+ PIPELINE_BUILD_TIMESTAMP: ${{ inputs.build_timestamp }}
+ PIPELINE_BASELINE_REF: master
+ PIPELINE_MODE: ${{ inputs.mode }}
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - uses: r-lib/actions/setup-r@v2
+ with:
+ r-version: '4.6.1'
+ use-public-rspm: true
+
+ - name: Restore the pinned pipeline library
+ run: |
+ echo "762ec8acc2d2ccbe5a217859c846c2de83254e3948112b437dde7a0ad5b15999 data-raw/vendor/pak_0.11.1.tar.gz" | sha256sum --check
+ R CMD INSTALL data-raw/vendor/pak_0.11.1.tar.gz
+ Rscript -e 'pak::lockfile_install("data-raw/pkg.lock")'
+
+ - name: Inspect upstream source bytes
+ if: inputs.mode == 'refresh'
+ run: Rscript data-raw/refresh_sources.R
+
+ - name: Upload source-refresh proposal for human review
+ if: inputs.mode == 'refresh'
+ uses: actions/upload-artifact@v4
+ with:
+ name: source-refresh-${{ inputs.data_version }}
+ path: data-raw/refresh-candidate/${{ inputs.data_version }}/
+ if-no-files-found: error
+
+ - name: Deterministic rebuild, pass one
+ if: inputs.mode == 'rebuild'
+ run: |
+ Rscript data-raw/run_pipeline.R
+ sha256sum \
+ data-raw/release/zipcodeR-data-${{ inputs.data_version }}.rds \
+ data-raw/release/manifest-${{ inputs.data_version }}.json \
+ data-raw/release/zipcodeR-reproducibility-${{ inputs.data_version }}.tar.gz \
+ > /tmp/first.sha256
+
+ - name: Deterministic rebuild, pass two
+ if: inputs.mode == 'rebuild'
+ run: |
+ Rscript data-raw/run_pipeline.R
+ sha256sum --check /tmp/first.sha256
+
+ - name: Upload unpublished release candidates
+ if: inputs.mode == 'rebuild'
+ uses: actions/upload-artifact@v4
+ with:
+ name: zipcodeR-data-${{ inputs.data_version }}-unpublished
+ path: data-raw/release/
+ if-no-files-found: error
diff --git a/.github/workflows/test-coverage.yaml b/.github/workflows/test-coverage.yaml
new file mode 100644
index 0000000..47bc63b
--- /dev/null
+++ b/.github/workflows/test-coverage.yaml
@@ -0,0 +1,55 @@
+# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
+on:
+ push:
+ branches: [main, master]
+ pull_request:
+ branches: [main, master]
+
+name: test-coverage
+
+permissions: read-all
+
+jobs:
+ test-coverage:
+ runs-on: ubuntu-latest
+ env:
+ GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: r-lib/actions/setup-r@v2
+ with:
+ use-public-rspm: true
+
+ - uses: r-lib/actions/setup-r-dependencies@v2
+ with:
+ extra-packages: any::covr, any::xml2
+ needs: coverage
+
+ - name: Test coverage
+ run: |
+ cov <- covr::package_coverage(
+ quiet = FALSE,
+ clean = FALSE,
+ install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package")
+ )
+ print(cov)
+ covr::to_cobertura(cov)
+ shell: Rscript {0}
+
+ - uses: codecov/codecov-action@v4
+ with:
+ # Fail if error if not on PR, or if on PR and token is given
+ fail_ci_if_error: ${{ github.event_name != 'pull_request' || secrets.CODECOV_TOKEN }}
+ files: ./cobertura.xml
+ plugins: noop
+ disable_search: true
+ token: ${{ secrets.CODECOV_TOKEN }}
+
+ - name: Show testthat output
+ if: always()
+ run: |
+ ## --------------------------------------------------------------------
+ find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true
+ shell: bash
diff --git a/.gitignore b/.gitignore
index 88937e0..c09eb6b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,5 @@
.Rhistory
.DS_Store
README.knit.md
+README.html
+vignettes/*.html
diff --git a/AUDIT.md b/AUDIT.md
new file mode 100644
index 0000000..6f789a1
--- /dev/null
+++ b/AUDIT.md
@@ -0,0 +1,270 @@
+# zipcodeR Phase 0 Audit
+
+> **Historical discovery document.** This audit predates differential testing
+> against the installed 0.3.5 package. That later testing showed that replacing
+> datasets, distance algorithms, conditions, and side effects would break the
+> research-reproducibility contract. Recommendations below such as removing
+> `raster`, changing distance math, or refreshing bundled data are therefore
+> superseded for the 0.4.0 legacy API. They may inform the explicit `_ng` API or
+> a future major release only; see `ROADMAP.md` and `NEWS.md` for current policy.
+
+**Date:** 2026-08-24 · **Branch:** `modernization-2026` · **Package version audited:** 0.3.5 (master @ 48ed689)
+
+This document records the discovery phase of the modernization effort: the current state of
+the package, its dependency tree, the provenance and reproducibility of its bundled data,
+the exact transformation contract between upstream and the shipped datasets, and a
+classification of every data-related issue report. **No code changes accompany this
+document.** The recommended branch decision for Phase 2 is at the end.
+
+---
+
+## 1. R CMD check --as-cran baseline
+
+_(Environment: R 4.6.1 aarch64-apple-darwin, Homebrew; all Imports/Suggests installed from
+source.)_
+
+`R CMD check --as-cran zipcodeR_0.3.5.tar.gz` → **Status: 1 ERROR, 2 WARNINGs, 3 NOTEs**,
+of which the package-substantive findings are:
+
+- **WARNING (CRAN incoming):** `Package CITATION file contains call(s) to old-style
+ citEntry(). Please use bibentry() instead.` → Phase 1 fix.
+- **WARNING (CRAN incoming):** `Insufficient package version (submitted: 0.3.5, existing:
+ 0.3.5)` → expected pre-bump; resolved by the Phase 1 version increment.
+- **NOTE (top-level files):** `AUDIT.md`, `CLAUDE.md` non-standard at top level → add to
+ `.Rbuildignore` in Phase 1.
+
+Environmental noise on this machine (not package defects): PDF-manual ERROR/WARNING
+(`pdflatex is not available` — no TeX installed), HTML-validation NOTE (old HTML Tidy),
+and a `zipcodeR-manual.tex` leftover NOTE consequent on the missing TeX. No errors,
+warnings, or notes in code checks, examples, tests, or vignette rebuilds.
+
+**Test suite baseline** (`devtools::test()`): **51 PASS / 0 FAIL / 28 WARN** — every
+warning is the tidyselect `.data`-in-`select()` deprecation (§5 misc hygiene).
+
+## 2. Dependency audit
+
+### 2.1 Declared dependencies (DESCRIPTION @ 0.3.5)
+
+- **Depends:** R (>= 3.5.0)
+- **Imports:** rlang, stringr, raster, tidycensus, tidyr, dplyr, jsonlite, httr, curl, RSQLite, DBI (no version pins)
+- **Suggests:** knitr, rmarkdown, markdown, readr, testthat (>= 3.0.0), covr, tibble
+- **Undeclared but used:** `utils` (`utils::download.file`, `utils::globalVariables`)
+
+### 2.2 What each import is actually used for
+
+| Package | Every usage site | Verdict |
+|---|---|---|
+| `raster` | `raster::pointDistance()` only — `R/zip_helper_functions.R:102` (`zip_distance`), `R/zip_lookups.r:364` (`search_radius`) | **Remove.** Replace with ~10-line vectorized haversine (base R). |
+| `tidycensus` | `tidycensus::fips_codes` dataset only — `R/zip_lookups.r:185` (`search_fips`), `:249` (`get_cd`). Imported *wholesale* (`@import tidycensus` → `import(tidycensus)` in NAMESPACE). | **Remove.** Vendor the ~3,200-row FIPS table (public-domain Census data) as package data via a `data-raw/` script. |
+| `tidyr` | `tidyr::extract()` once, inside `normalize_zip()`'s `capture_group()` helper (`R/zip_helper_functions.R:16`) | **Remove.** One base-R regex call. |
+| `jsonlite`, `httr`, `curl`, `RSQLite`, `DBI` | `download_zip_data()` only (`R/download_data.r`) | **Demote/remove** with the `download_zip_data()` redesign (see §5.4) — either Suggests + `rlang::check_installed()` or dropped entirely when the refresh moves to `data-raw/`. |
+| `dplyr` | Pervasive in all lookup functions | Keep. |
+| `rlang` | `.data` pronoun; `list2()` at `zip_lookups.r:38` | Keep (shrink usage). |
+| `stringr` | `str_detect` in search functions | Keep (or base-R; low priority). |
+
+**End state:** `Imports: dplyr, rlang, stringr, utils` — zero GDAL, zero arrow, zero
+retired-lineage packages at load time.
+
+### 2.3 The legacy-stack chains (issues #21, #24, #28)
+
+- `raster` → **`sp`** (emits the rgdal/rgeos retirement startup message = issue **#28**) and
+ → `terra` (links GDAL/GEOS/PROJ system libraries). The #21 load failure
+ ("`coerce` … Raster, SpatRaster") is a classic stale-binary collision between `raster`
+ and `terra` — impossible once `raster` is gone.
+- `tidycensus` → **`sf`** (GDAL at load; on distros whose GDAL links libarrow this produces
+ the issue **#24** `libarrow.so.800` warnings) plus tigris, rvest, units, and a long tail.
+- Neither chain is needed: one is two distance calls, the other is a static lookup table.
+- Measured cost on this machine (R 4.6.1/arm64): attaching `raster` + `tidycensus` takes
+ ~2.6 s and loads **54 namespaces** vs 21 for a dplyr-only baseline — all incurred by
+ `library(zipcodeR)` today. (Modern `sp` ≥2.x no longer prints the #28 retirement
+ banner, but the GDAL/PROJ/GEOS load-time exposure and install burden remain.)
+
+## 3. Upstream reproducibility (uszipcode-project)
+
+### 3.1 Verdict: build scripts for the current data are NOT public — reconstruct
+
+- `MacHu-GWU/uszipcode-project` (MIT license, last data release Jan 2022) today contains
+ only the client library (`uszipcode/model.py`, `search.py`, `db.py` which downloads the
+ prebuilt SQLite from GitHub releases).
+- Full git history (82 commits, back to 2015) was scanned. A deleted `dataset/` directory
+ was recovered from the parent of the `1.0.1 pre CI commit` (`d4ede94^`), containing
+ `step1_geocoding.py`, `step2_merge_zipcode_data.py`, `step3_make_database.py` plus two
+ source archives (`federalgovernmentzipcodes.zip` from federalgovernmentzipcodes.us,
+ 2012 vintage; `zcta2010.zip` Census 2010 ZCTA data). **These are the v0.0.8-era (2016)
+ scripts** for the *old* schema (IRS wages, Google-geocoded bounds) — not the pipeline
+ that produced the 2021/2022 `simple_db`/`comprehensive_db` (ACS-era schema with
+ median_household_income, timezone, area codes, …). They also depend on dead packages
+ (`geomate`, `sqlite4dummy`) and personal Google API keys. Recovered copies are archived
+ for provenance but are **not runnable and do not produce the current schema**.
+- No sibling/crawler repo exists under the author's account; the data.census.gov crawler
+ behind the 1.0.1 release was never published.
+
+**Conclusion:** the Phase 2 pipeline must be a reconstruction in R from primary sources,
+using the upstream SQLite snapshots as a *validation reference*, not as a build input.
+MIT licensing on upstream and public-domain status of the underlying Census/IRS data make
+this unproblematic; attribution to uszipcode-project stays in the docs.
+
+### 3.2 Upstream snapshots (validation references)
+
+| Release | Asset | Size | SHA256 | Rows (`simple_zipcode`) |
+|---|---|---|---|---|
+| `0.2.6-db-file` (2021-06-08) | simple_db.sqlite | 9,965,568 | `f7c1c9461f9ef648e83e243e6bbe4abe1d116e70e6e4a92aa52f9ea2ae80b3ba` | **41,877** |
+| `1.0.1.db` (2022-01-05) | simple_db.sqlite | 10,727,424 | `43383f108ef14dccd925107bc77705f622b1014111ad5c9e5e2a6837bb7f64ff` | **42,724** |
+| `1.0.1.db` (2022-01-05) | comprehensive_db.sqlite | 456,556,544 | `d85ed4e25884bc27bdd339d57dd9e2d1763531d4c050acb7a05a3d5aca90668d` | 42,724 (see §3.4) |
+
+Set difference: 1.0.1 = 0.2.6 + 847 new ZIPs (787 MILITARY — a type absent from 0.2.6 —
+plus 30 PO BOX, 20 STANDARD, 10 UNIQUE), **zero ZIPs dropped**.
+
+⚠ Value-compat caveat: 0.2.6 stores `zipcode_type` titleized (`Standard`, `PO Box`,
+`Unique`); 1.0.1 switched to uppercase (`STANDARD`, `PO BOX`, `UNIQUE`, `MILITARY`).
+Shipped `zip_code_db` has the titleized values, and `reverse_zipcode()` tests match on
+them — any refresh from 1.0.1-schema data must normalize case to preserve the contract.
+
+### 3.3 simple_zipcode schema (both releases identical)
+
+24 columns: `zipcode` (TEXT, PK), `zipcode_type`, `major_city`, `post_office_city`,
+`common_city_list` (BLOB: zlib-compressed JSON array), `county`, `state`, `lat`, `lng`
+(FLOAT, indexed), `timezone`, `radius_in_miles`, `area_code_list` (BLOB: zlib JSON),
+`population`, `population_density`, `land_area_in_sqmi`, `water_area_in_sqmi`,
+`housing_units`, `occupied_housing_units`, `median_home_value`,
+`median_household_income`, `bounds_west/east/north/south`.
+
+Most plausible primary sources per field (for the reconstruction pipeline):
+
+| Field(s) | Source |
+|---|---|
+| zipcode, zipcode_type, major_city, post_office_city, common_city_list | USPS ZIP data (via a licensed-free mirror such as the HUD-USPS crosswalk for existence/city, GeoNames, or federalgovernmentzipcodes-style compilations; needs a licensing check — see §7) |
+| county, state | USPS city/state + Census county assignment of the ZCTA |
+| lat, lng, bounds_* , land/water_area | Census 2020 ZCTA Gazetteer + TIGER/Line ZCTA shapefiles |
+| population, population_density, housing_units, occupied_housing_units, median_home_value, median_household_income | Census ACS 5-year at ZCTA level (tables B01003, B25001/2, B25077, B19013) |
+| timezone | point-in-polygon of centroid vs IANA tz boundaries (e.g. timezone-boundary-builder data) |
+| radius_in_miles | derived: sqrt(land_area/π) or bounds-based |
+| area_code_list | NANPA area-code data (or drop-forward as frozen; roadmap decision) |
+
+### 3.4 comprehensive_db schema
+
+Two tables: `simple_zipcode` (identical to the simple DB) and `comprehensive_zipcode`
+(42,724 rows, 54 columns): the same 24 base columns, plus `polygon` (ZCTA boundary,
+compressed JSON) and 29 rich ACS-profile columns stored as zlib-compressed JSON blobs —
+`population_by_year`, `population_by_age/gender/race`, `head_of_household_by_age`,
+`families_vs_singles`, `households_with_kids`, `children_by_age`, `housing_type`,
+`year_housing_was_built`, `housing_occupancy`, `vacancy_reason`,
+`owner_occupied_home_values`, `rental_properties_by_number_of_rooms`,
+`monthly_rent_including_utilities_{studio,1b,2b,3plus_b}`, `employment_status`,
+`average_household_income_over_time`, `household_income`, `annual_individual_earnings`,
+four `sources/investment/retirement_income` pairs, `source_of_earnings`,
+`means_of_transportation_to_work_for_workers_16_and_over`,
+`travel_time_to_work_in_minutes`, `educational_attainment_for_population_25_and_over`,
+`school_enrollment_age_3_to_17`. All are data.census.gov ACS profile aggregates keyed by
+ZCTA — the source set for the Phase 2 "comprehensive" release asset and the #7 FIPS/
+enrichment roadmap items.
+
+## 4. The transformation contract (upstream → zip_code_db)
+
+`download_zip_data()` (`R/download_data.r:80-83`) reveals the original derivation:
+
+```r
+zip_code_db <- DBI::dbGetQuery(conn, "SELECT * FROM simple_zipcode")
+save(zip_code_db, file = ...)
+```
+
+i.e. an **identity transformation**: all 24 columns, upstream order, RSQLite default type
+mapping (TEXT→character, FLOAT→numeric, INTEGER→integer, BLOB→`blob` list-column of
+zlib-compressed raw vectors), rows in SQLite storage order, class `data.frame`.
+`R/sysdata.rda` stores `zip_code_db_version = "2021-06-08"` — exactly the publish date of
+the `0.2.6-db-file` release, confirming the shipped data is the **0.2.6 snapshot** (41,877
+rows match; row 1 = 35004 Moody AL matches; `zipcode_type` titleization matches).
+
+**Verified 2026-08-24:** loading the 0.2.6 `simple_db.sqlite` via
+`DBI::dbGetQuery(conn, "SELECT * FROM simple_zipcode")` and comparing to the shipped
+`data/zip_code_db.rda` yields `identical() == TRUE` — zero differences per
+`waldo::compare()`. The compatibility contract is therefore precisely: *the 24-column
+`simple_zipcode` schema, RSQLite default type mapping, SQLite storage order, class
+`data.frame`, titleized `zipcode_type` values, blob list-columns for the two JSON fields.*
+
+Note the `blob` class on `common_city_list`/`area_code_list` exists only because the
+data was built through RSQLite; nothing in Imports provides the `blob` package that
+defines the class, so those columns print as raw zlib bytes for end users. This is an
+existing wart to address (with compatibility care) in the data-pipeline phase.
+
+The other two datasets:
+
+- `zcta_crosswalk` (tibble, 148,897 × 3: ZCTA5, TRACT, GEOID) — selected columns of the
+ Census **2010** ZCTA↔tract relationship file. 2010 vintage; a 2020 refresh changes
+ GEOIDs (Phase 2, with a documented migration note).
+- `zip_to_cd` (data.frame, 45,914 × 2: ZIP, CD = state FIPS + district) — HUD-USPS
+ crosswalk vintage pre-2020 redistricting (issue #29). PR #30 (@awallender) demonstrates
+ the modern method with the Census 2020 CD118↔ZCTA relationship file; Phase 2 should use
+ the same method with the current CD119 file and credit the contribution.
+
+## 5. Function-level defect inventory
+
+(Each maps to a Phase 1/3 fix with a regression test and a staged issue comment.)
+
+| # | Function / site | Defect |
+|---|---|---|
+| #27 | `reverse_zipcode()` `R/zip_lookups.r:89-118` | **Reproduced on master 2026-08-24**: the issue's 13-row `mutate()` reprex fails with `county must be size 13 or 1, not 12`, and `reverse_zipcode(c("08734","08731"))` returns 08731 first (database order). Output ordered by database order, not input order (`%in%` filter); **duplicate inputs collapse to one row**, breaking `mutate()`'s length contract (the reported error). NA-row insertion is an O(n²) `add_row` loop; `.data` misused inside `stop()` at :115 (would itself error); scalar-only length check at :91-96 skips vectors. |
+| #20 | `zip_distance()` `R/zip_helper_functions.R:80-116` | Reported swap **already fixed in 0.3.4 — verified on master 2026-08-24** with both reprexes from the issue: `zip_distance(c("08731","08734"), c("08901","08005"))` → 40.70 / 8.06 mi (correct pairing) and the repeated-pair case → 0.0 / 6.9 (correct). Phase 1 = regression tests + staged closing comment. Remaining latent defects: relies on `zipcode` uniqueness with no guard; `filter(lat != "NA")` string comparison at :90; `lonlat = FALSE` documents planar distance on raw degrees (meaningless units). |
+| #33 | `search_radius()` `R/zip_lookups.r:355-383` | ~42k-iteration R loop calling `raster::pointDistance` per row (reporter measured ~40× speedup from vectorizing alone). Additional bug at :360: `filter(lat != "NA")` resolves `lat` to the *function argument*, so the intended NA-coordinate filter is a no-op. Phase 3: vectorized haversine + bounding-box prefilter. |
+| — | `geocode_zip()` `R/zip_lookups.r:324-340` | Same order bug as reverse_zipcode; silently drops unmatched ZIPs (output shorter than input). |
+| — | `download_zip_data()` `R/download_data.r` | Writes into the installed package directory via `system.file()` — CRAN policy violation, fails on read-only libraries, and under `LazyData: true` the written `.rda` is **never loaded** (installed data lives in `data/Rdata.rdb`), so the refresh mechanism has never actually worked post-install. All `file.exists(system.file("data", "*.rda"))` guards are permanently FALSE on installed packages. Crosswalk URLs point at the abandoned `gavinrozzi/zipcodeR-data` repo. Internet check happens *after* the first network call. |
+| — | `get_cd()` `R/zip_lookups.r:261-264` | Computed `output` (with a fragile hardcoded rename) discarded; returns a bare `list()`. Also `nchar(county_fips < 3)` misplaced parenthesis at :197 (`search_fips`) — always-true condition, harmless only by accident. |
+| #14 | `geocode_zip()` docs | Not a bug: negative longitude is the western hemisphere sign convention. Fix via docs + pkgdown FAQ. |
+| #13 | data access | `zip_code_db not found` reports trace to broken/partial installs of lazy-loaded data; add FAQ + `R CMD check`-clean reinstall guidance. |
+| — | Misc hygiene | tidyselect `.data`-in-`select()` deprecation warnings (`zip_lookups.r:287,332,374`, `zip_helper_functions.R:89`); deprecated `citEntry()` in `inst/CITATION`; `test-03-helper-functions.R` assertions outside `test_that()`; duplicated `%>%` importFrom; `R/data.r:20` documents `zipcode_type` as "2010 State FIPS Code" (copy-paste error). |
+| — | CI | `R-CMD-check.yaml` uses r-lib/actions **v1** on retired `ubuntu-20.04` images with focal RSPM URLs; no oldrel job. `pkgdown.yaml` is already modern (v2). |
+
+## 6. Missing-ZIP forensics (#25, #26, #19)
+
+Authoritative reference: distinct 2020 ZCTAs from the Census CD118↔ZCTA national
+relationship file = **33,791 ZCTAs**.
+
+| Report | ZIP(s) | Shipped 0.2.6 data | Upstream 1.0.1 | 2020 ZCTA? | Classification |
+|---|---|---|---|---|---|
+| #26 | 91230 (Glendale CA) | absent | absent | **no** | USPS-only (PO-Box-type) ZIP, never covered by upstream's ZCTA-centric sources. Genuinely valid ZIP; needs a USPS-derived source (HUD crosswalk has it) in the reconstruction pipeline. |
+| #25 | 97003 (Beaverton OR) | absent | present but **lat/lng = 0.0** | yes | Missing upstream in 0.2.6; added in 1.0.1 with a bad (0,0) geocode. Fix = reconstruction with Census Gazetteer coordinates. The "dozen other Oregon ZIPs" were never enumerated in the issue (empty body); Oregon otherwise fully covered — 97003 is the *only* 2020 OR ZCTA absent from shipped data. |
+| #19 | "many ZIPs" in `zip_distance` | — | — | — | Two causes: (a) staleness above; (b) **8,773 of 41,877 shipped rows have NULL lat/lng** (PO Box/Unique ZIPs without geocodes) and `zip_distance` returns NA for them. Documentation + data-refresh issue, not a code bug. |
+| — | All 2020 ZCTAs | 23 missing nationwide | **0 missing** | — | The 23: US Virgin Islands (00802/20/30/40/50/51), American Samoa (96799), Guam (96910-96929), N. Mariana (96950-52), and 5 new mainland ZCTAs (72405, 72713, 75036, 75072, 89437) + 97003. Data refresh resolves all. |
+
+## 7. Licensing & provenance notes
+
+- **uszipcode-project:** MIT — reuse/derivation fine with attribution (already credited in
+ README/docs; keep it).
+- **Census (ACS, decennial, gazetteer, TIGER, relationship files), HUD-USPS crosswalk
+ (free registration/API token), IRS SOI:** U.S. Government public domain / freely
+ redistributable. HUD crosswalk requires a token for API access; files are
+ redistributable with citation.
+- ⚠ **Open question for the maintainer:** USPS city/alias names (`major_city`,
+ `post_office_city`, `common_city_list`, `zipcode_type`). USPS licenses its raw products;
+ the public compilations upstream used (federalgovernmentzipcodes.us, 2012) are of
+ unclear provenance. The reconstruction pipeline can source ZIP existence/type/city from
+ the HUD-USPS crosswalk (public) + GeoNames (CC-BY) instead. **Flagged per the
+ constraint: decide the acceptable source before Phase 2 builds these columns.**
+
+## 8. Recommended Phase 2 branch decision
+
+**Reconstruct the pipeline in R** (`data-raw/`, targets-style staged scripts):
+
+1. Primary sources: Census 2020 Gazetteer + ACS 5-yr (ZCTA level), HUD-USPS crosswalk,
+ Census relationship files (tract, CD119), tz-boundary data. Pinned URLs + SHA256.
+2. Output the exact 24-column contract of §4 (titleized `zipcode_type`, blob-compatible
+ list columns or a documented forward-compatible representation), validated against
+ both upstream snapshots (0.2.6 for backward compat, 1.0.1 for coverage).
+3. Validation gate: row count ≥ 41,877 and ~42-43k sanity band; every shipped ZIP retained
+ (retirements flagged, never dropped); regression ZIPs (91230, 97003, VI/Guam set)
+ present with coordinates; schema identical; #20-reprex distance spot-checks.
+4. Interim quick win available at any time: the 1.0.1 snapshot itself fixes 22 of 23
+ missing ZCTAs (not 91230) — but it is *also* 4 years stale; prefer going straight to
+ reconstruction.
+
+## 9. Risks
+
+- USPS-derived naming columns need a licensing decision (§7) before they can be refreshed.
+- `zipcode_type`/military ZIPs: introducing MILITARY rows changes `search_*` result sets —
+ additive, but should be release-noted as data (not API) change.
+- blob list-columns: exotic (`blob` class from an implicit RSQLite dependency at build
+ time); consider migrating to plain character-JSON or list columns in a *data-major*
+ release with a compatibility shim, since the `blob` class currently arrives without the
+ package that defines it being declared anywhere.
+- 2020 ZCTA/tract GEOID changes will shift `get_tracts()` results — document as data
+ vintage change.
diff --git a/CLAUDE.md b/CLAUDE.md
index 9069528..aea81ce 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,60 +1,76 @@
# CLAUDE.md
-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+This file provides repository guidance for coding agents.
-## Package Overview
+## Package contract
-zipcodeR is an R package that simplifies working with U.S. ZIP codes. It provides an offline database of 41,877 ZIP codes with 24 attributes each, plus functions for geographic search, distance calculations, and Census data integration.
+zipcodeR provides offline U.S. ZIP-code lookups and basic spatial analysis.
+The existing exported API is a frozen research-reproducibility contract: legacy
+functions, conditions, ordering, side effects, and the three bundled datasets
+must remain exactly compatible with zipcodeR 0.3.5.
-## Development Commands
+Corrected behavior and refreshed data are opt-in through the `_ng` functions.
+Every data-dependent `_ng` function takes an explicit, validated data bundle as
+its first argument. Never add an implicit "latest" version, automatic download,
+global option, or fallback to the bundled legacy data.
+
+## Development commands
```bash
-# Run all tests
devtools::test()
-
-# Run a single test file
-testthat::test_file("tests/testthat/test-01-zip-lookups.R")
-
-# Check package (as CRAN would)
-devtools::check()
-
-# Build documentation from roxygen comments
+Rscript tools/compatibility-check.R --baseline-ref=origin/master
+Rscript bench/search_radius_bench.R
+devtools::check(args = "--as-cran")
devtools::document()
-
-# Install package locally for testing
-devtools::install()
-
-# Load package during development (without installing)
-devtools::load_all()
```
## Architecture
-### Data Layer (`data/`)
-Three bundled `.rda` datasets loaded lazily:
-- `zip_code_db` - Main ZIP code database (41,877 rows, 24 columns)
-- `zcta_crosswalk` - ZCTA-to-Census Tract mapping (148,897 rows)
-- `zip_to_cd` - ZIP-to-Congressional District mapping (45,914 rows)
-
-### Source Files (`R/`)
-- `data.r` - Roxygen documentation for the three datasets
-- `zip_lookups.r` - 14 search/lookup functions that filter the datasets
-- `zip_helper_functions.R` - Utility functions: `normalize_zip()`, `zip_distance()`, `geocode_zip()`
-- `download_data.r` - `download_zip_data()` to fetch updated data from upstream
-- `globals.r` - Global variable declarations for NSE compliance
-
-### Function Patterns
-All lookup functions return tibbles and accept vectors for batch operations. They use tidyverse-style programming with dplyr and the `.data` pronoun for NSE.
-
-## Testing
-
-Tests are in `tests/testthat/` using testthat v3:
-- `test-01-zip-lookups.R` - Tests for all 14 lookup functions
-- `test-02-data.R` - Data integrity tests
-- `test-03-helper-functions.R` - Utility function tests
-
-## CI/CD
-
-GitHub Actions workflows in `.github/workflows/`:
-- `R-CMD-check.yaml` - Runs `R CMD check` on Windows, macOS, and Ubuntu with multiple R versions
-- `pkgdown.yaml` - Builds and deploys documentation site to GitHub Pages
+### Frozen legacy data (`data/`)
+
+- `zip_code_db`: 41,877 rows and 24 columns, data date 2021-06-08.
+- `zcta_crosswalk`: 148,897 legacy ZCTA-to-tract relationships.
+- `zip_to_cd`: 45,914 legacy ZIP-to-district relationships.
+
+These objects must remain `identical()` to the 0.3.5 objects. Modern data is
+never written into `data/`.
+
+### R code (`R/`)
+
+- `zip_lookups.r`, `zip_helper_functions.R`, and `download_data.r` implement
+ the frozen legacy contract. Legacy distance calculations intentionally retain
+ `raster::pointDistance()` WGS84 behavior.
+- `ng_functions.R` implements corrected lookups and modern haversine distance
+ calculations against an explicit bundle.
+- `data_bundle.R` validates, reads, downloads, and reports metadata for pinned
+ data bundles.
+- `data_version.R` contains the legacy metadata and comprehensive-asset API.
+- `distance.R` is an internal helper used only by the `_ng` path.
+
+`download_zip_data()` intentionally retains its 0.3.5 observable behavior.
+Discourage it in documentation only during this compatibility release.
+
+### External data pipeline (`data-raw/`)
+
+The pipeline creates versioned data-only release assets, not package datasets.
+Deterministic rebuilds require `PIPELINE_MODE=rebuild`, an explicit
+`PIPELINE_DATA_VERSION`, and an explicit `PIPELINE_BUILD_TIMESTAMP`. Exact raw
+responses, source checksums, the dependency lock, and the build container are
+archived with each release. `refresh_sources.R` proposes new source pins but
+does not build or publish anything.
+
+## Tests and release gates
+
+- `tools/compatibility-check.R` installs the baseline and candidate into
+ isolated libraries and compares datasets, results, conditions, and formals.
+- `tests/testthat/test-04-data-bundles.R` covers integrity and download failure
+ cases.
+- `tests/testthat/test-05-ng-api.R` covers the corrected API contract.
+- `.github/workflows/R-CMD-check.yaml` runs compatibility and normal package
+ checks on Windows, macOS, Ubuntu release/devel/oldrel.
+- `.github/workflows/refresh-data.yaml` is manual. It can refresh source
+ proposals or run a two-pass deterministic rebuild, but never publishes.
+
+Do not claim a data version is available or enable its downloader until its
+release and checksum-verified asset are public and smoke-tested from a clean
+machine.
diff --git a/DESCRIPTION b/DESCRIPTION
index e0d8557..cb11263 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -1,6 +1,6 @@
Package: zipcodeR
Title: Data & Functions for Working with US ZIP Codes
-Version: 0.3.5
+Version: 0.4.0
Authors@R:
person(given = "Gavin",
family = "Rozzi",
@@ -9,15 +9,17 @@ Authors@R:
comment = c(ORCID = "0000-0002-9969-8175"))
Description: Make working with ZIP codes in R painless with an integrated dataset of U.S. ZIP codes and functions for working with them.
Search ZIP codes by multiple geographies, including state, county, city & across time zones. Also included are functions for relating
- ZIP codes to Census data, geocoding & distance calculations.
+ ZIP codes to Census data, geocoding & distance calculations. New
+ analyses can select an immutable modern data bundle through the
+ next-generation API, while the historical interface remains
+ compatible with version 0.3.5 for reproducible research.
License: GPL-3
URL: https://github.com/gavinrozzi/zipcodeR/, https://www.gavinrozzi.com/project/zipcoder/
BugReports: https://github.com/gavinrozzi/zipcodeR/issues/
Encoding: UTF-8
LazyData: true
Roxygen: list(markdown = TRUE)
-RoxygenNote: 7.2.1
-Imports:
+Imports:
rlang,
stringr,
raster,
@@ -28,16 +30,19 @@ Imports:
httr,
curl,
RSQLite,
- DBI
-Depends:
+ DBI,
+ tools
+Depends:
R (>= 3.5.0)
Suggests:
+ openssl,
knitr,
rmarkdown,
markdown,
readr,
- testthat (>= 3.0.0),
+ testthat (>= 3.2.0),
covr,
tibble
VignetteBuilder: knitr, rmarkdown
Config/testthat/edition: 3
+Config/roxygen2/version: 8.1.0
diff --git a/ISSUE-RESOLUTION.md b/ISSUE-RESOLUTION.md
new file mode 100644
index 0000000..d8464f7
--- /dev/null
+++ b/ISSUE-RESOLUTION.md
@@ -0,0 +1,44 @@
+# PR #34 open-issue resolution matrix
+
+This is the review record for every issue open on 2026-08-30. “Addressed” does
+not always mean changing a historical function: the release contract keeps
+unsuffixed 0.3.5 calls reproducible, recommends `_ng` for new analyses, and
+documents reports that are data-model limitations or invalid assumptions.
+
+| Issue | Resolution in PR #34 | Acceptance evidence | Release disposition |
+|---|---|---|---|
+| #7 FIPS in reverse lookup | `reverse_zipcode_ng()` returns two-digit `state_fips` and full five-digit `county_fips` for the predominant county. The legacy output schema is frozen. | `_ng` regression test includes Ocean County `34029`; vendored FIPS table includes Alexandria city `51510`. | Close after package and first bundle ship. |
+| #13 `zip_code_db` not found | Package data remains namespace-accessible and byte-identical to 0.3.5; FAQ gives reinstall guidance for a broken lazy-load database. | Clean installed-package calls and the isolated compatibility harness invoke lookup exports without assigning the dataset globally. | Close as stale/documented after release. |
+| #14 negative longitude | Negative values are the correct west-of-Greenwich convention. | `?geocode_zip`, data documentation, and FAQ explain the sign; no data mutation is made. | Close as answered. |
+| #19 missing ZIP distances | Modern bundles refresh authoritative ZCTA coordinates; USPS-only ZIPs without defensible coordinates remain unavailable with provenance instead of proxy points. | Pipeline coordinate gate, record quality sidecar, missing-coordinate `_ng` tests, and explicit bundle version metadata. | Close when 0.4.0 ships, with the limitation noted. |
+| #20 vector distance order | The reporter's swap was fixed in 0.3.4; 0.4.0 freezes the 0.3.5 result. `_ng` additionally specifies input-order, duplicate, recycling, and missing-coordinate behavior. | Exact differential cases plus `_ng` pairing/recycling tests. | Close after release. |
+| #21 `raster`/`terra` load failure | `raster` remains installed for exact legacy WGS84 calls but is no longer imported at startup; a legacy distance call loads it lazily. | Clean installed process loads none of `raster`, `terra`, `sp`, `sf`, or `tidycensus`, then returns legacy distance 40.70 exactly. | Close after release. |
+| #24 GDAL `libarrow` warnings | `tidycensus`/`sf` and `raster` are no longer loaded by `library(zipcodeR)`; legacy functions load their namespace only when needed. | Clean installed-process namespace inspection and NAMESPACE regression test. | Close after release. |
+| #25 missing `97003` | The modern pipeline corroborates `97003` as a Census ZCTA and publishes its authoritative internal point only in the versioned bundle. | Public bundle contains `97003`, Census coordinate quality `authoritative`, and ACS quality `authoritative_current_vintage`. | Close when 0.4.0 ships. |
+| #26 `91230` | The stated San Diego ZIP is `92130`, which is present. `91230` is absent from pinned Census ZCTA/ACS sources and remains quarantined rather than receiving a Glendale proxy centroid. | Pipeline quarantine list, absence checks in archived sources, and validation gate prohibiting uncorroborated additions. | Close as corrected/explained; reconsider only with authoritative USPS evidence. |
+| #27 duplicates in `reverse_zipcode()` | Historical behavior remains reproducible. `reverse_zipcode_ng()` and `geocode_zip_ng()` return one row per input in input order, preserving duplicates and explicit misses. | `_ng` tests cover duplicate-valid and missing ZIPs; differential gate locks the legacy condition and output. | Close after bundle publication, explicitly noting the legacy behavior. |
+| #28 legacy spatial warning | Spatial namespaces are lazy rather than startup imports; `_ng` distance code never invokes them. | Same clean-load evidence as #21/#24, plus exact legacy distance differential tests. | Close after release. |
+| #29 outdated districts | The modern bundle uses the pinned Census 119th-Congress ZCTA relationship and refuses city-wide inference for USPS-only ZIPs. Legacy mappings remain unchanged. | Source SHA in manifest, mapping-policy validation, authoritative and unmapped `_ng` tests; `08731` maps to NJ-02 and NJ-04 in the public bundle. | Close when 0.4.0 ships. |
+| #32 jurisdiction accuracy | ZIP city is a mailing name and ZCTA is not a municipal boundary; ZIP-only input cannot yield authoritative address jurisdiction. | Data docs and FAQ direct users to full-address geocoding against boundary files and label county as predominant. | Close as answered/documented. |
+| #33 radius performance | One vectorized legacy WGS84 call plus conservative prefilter replaces the per-row loop. | Benchmark first requires `identical()` result; differential tests include boundaries and antimeridian cases. Representative local calls are roughly 10–15 ms instead of about 1 second. | Close after required CI. |
+
+## Merge gates
+
+The issue-level code is complete only when all of the following are true:
+
+1. isolated 0.3.5 differential tests pass on all required CI platforms;
+2. normal vignette-building checks pass on Windows, macOS, Ubuntu release,
+ devel, and oldrel;
+3. a clean commit builds the modern bundle, manifest, and reproducibility
+ archive twice with identical hashes;
+4. the bundle, manifest, reproducibility archive, and comprehensive database
+ are public at immutable URLs and pass clean-machine checksum/schema calls;
+5. the verified bundle checksum is enabled in the package registry and CI is
+ rerun on that registry commit.
+
+Gates 3 and 4 passed for release `data-2026.08`: two clean builds matched bundle
+SHA `9059026c...`, manifest SHA `5628977c...`, and archive SHA `8b22ab07...`;
+all four public assets, including the comprehensive database, passed download
+and integrity checks. Gate 5 also passed: the explicit `2026.08` registry was
+enabled only after those checks, and the full required platform, coverage, and
+documentation matrix is green on the registry-enabled PR head.
diff --git a/NAMESPACE b/NAMESPACE
index 1ea32a9..5a4a4da 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -1,34 +1,54 @@
# Generated by roxygen2: do not edit by hand
+export(download_comprehensive_data)
export(download_zip_data)
+export(download_zip_data_bundle)
export(geocode_zip)
+export(geocode_zip_ng)
export(get_cd)
+export(get_cd_ng)
export(get_tracts)
+export(get_tracts_ng)
export(is_zcta)
+export(is_zcta_ng)
export(normalize_zip)
+export(normalize_zip_ng)
+export(read_zip_data_bundle)
export(reverse_zipcode)
+export(reverse_zipcode_ng)
export(search_cd)
+export(search_cd_ng)
export(search_city)
+export(search_city_ng)
export(search_county)
+export(search_county_ng)
export(search_fips)
+export(search_fips_ng)
export(search_radius)
+export(search_radius_ng)
export(search_state)
+export(search_state_ng)
export(search_tz)
+export(search_tz_ng)
+export(zip_data_provenance)
+export(zip_data_version)
export(zip_distance)
-import(tidycensus)
+export(zip_distance_ng)
importFrom(DBI,dbGetQuery)
importFrom(RSQLite,dbConnect)
importFrom(curl,has_internet)
-importFrom(dplyr,"%>%")
-importFrom(dplyr,`%>%`)
-importFrom(dplyr,filter)
-importFrom(dplyr,left_join)
-importFrom(dplyr,pull)
-importFrom(dplyr,tibble)
+importFrom(dplyr,
+ "%>%",
+ filter,
+ left_join,
+ pull,
+ tibble
+)
importFrom(httr,http_error)
importFrom(jsonlite,fromJSON)
-importFrom(raster,pointDistance)
-importFrom(rlang,.data)
-importFrom(rlang,list2)
+importFrom(rlang,
+ .data,
+ list2
+)
importFrom(stringr,str_detect)
importFrom(tidyr,extract)
diff --git a/NEWS.md b/NEWS.md
index e47bc57..dd96eb3 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,44 +1,129 @@
-# *News*
+# zipcodeR 0.4.0
+
+## Reproducibility contract
+
+- Every pre-existing exported function retains the zipcodeR 0.3.5 signature,
+ return value, class, attributes, row order, rounding, warnings, messages,
+ errors, and edge-case behavior. This deliberately includes historical
+ defects where changing them could alter published research.
+- The bundled `zip_code_db`, `zcta_crosswalk`, and `zip_to_cd` objects are
+ identical to 0.3.5: 41,877 ZIP rows, 148,897 ZCTA-to-tract rows, and 45,914
+ ZIP-to-district rows.
+- `zip_distance()` and `search_radius()` retain the legacy
+ `raster::pointDistance()` WGS84 calculation. `search_radius()` now invokes
+ that same calculation in a vectorized call; the differential gate requires
+ exact object identity with 0.3.5 before accepting the speedup.
+- `Depends: R (>= 3.5.0)` and the legacy runtime dependencies are retained.
+ `download_zip_data()` keeps its prior observable behavior. Documentation,
+ rather than a runtime warning or no-op, discourages its use.
+
+## Explicit next-generation API
+
+- The `_ng` API is the recommended interface for new analyses. Its suffix
+ makes the opt-in visible in code: callers choose newer, versioned data and
+ corrected semantics while existing unsuffixed calls stay historical.
+- Corrected behavior and modern data are opt-in through the `_ng`
+ functions: `search_state_ng()`, `search_county_ng()`, `search_city_ng()`,
+ `search_tz_ng()`, `search_fips_ng()`, `search_cd_ng()`,
+ `search_radius_ng()`, `reverse_zipcode_ng()`, `geocode_zip_ng()`,
+ `get_tracts_ng()`, `get_cd_ng()`, `is_zcta_ng()`, `normalize_zip_ng()`, and
+ `zip_distance_ng()`.
+- Every data-dependent `_ng` function requires an explicit validated data
+ bundle as its first argument. No lookup resolves a `latest` alias, consults
+ a global option, mutates the session, falls back to another vintage, or
+ performs an implicit network request.
+- `_ng` never means an unpinned "latest" dataset. A project selects and records
+ one immutable bundle version and checksum.
+- `download_zip_data_bundle(version)`, `read_zip_data_bundle(path)`,
+ `zip_data_version(x)`, and `zip_data_provenance()` support checksum-pinned,
+ offline, and auditable research workflows.
+- `_ng` lookups preserve input order and duplicates, represent missing rows
+ explicitly, validate inputs consistently, use modern geodesic behavior,
+ keep GEOIDs as character identifiers, and expose authoritative/unmapped
+ district status.
+- `reverse_zipcode_ng()` adds the two-digit state FIPS code and full five-digit
+ county FIPS code for the record's predominant county.
+- `raster` and `tidycensus` remain installed legacy dependencies, but are no
+ longer imported when `zipcodeR` starts. They load only when a legacy function
+ actually calls them, avoiding the historical `raster`/`terra` and GDAL/Arrow
+ startup failures without changing legacy results.
+
+## Versioned data assets
+
+- Refreshed data is no longer substituted into the package defaults. The
+ pipeline writes one external RDS bundle with dataset, metadata, provenance,
+ and quality sidecars, plus a manifest and reproducibility archive.
+- Static sources, the raw ACS response, and the derived ACS table are archived
+ and checksummed. GeoNames is pinned rather than floating. The data version
+ and build timestamp are explicit inputs; the full package dependency graph
+ is locked.
+- ZIP candidates absent from an authoritative ZCTA source are quarantined.
+ Proxy city points are not published as ZIP centroids. District assignments
+ use the Census relationship file only; USPS-only ZIPs without an
+ authoritative relationship remain unmapped with a reason.
+- Upstream inspection and deterministic rebuilds are separate workflow modes.
+ The workflow uploads unpublished candidates for review and never chooses a
+ version or publishes automatically.
+- The immutable `data-2026.08` bundle, manifest, deterministic reproducibility
+ archive, and comprehensive SQLite asset are public. Their release URLs and
+ checksums, representative `_ng` calls, manifest schema, and SQLite integrity
+ were independently smoke-tested before enabling the registries.
+
+## Tests, documentation, and infrastructure
+
+- `tools/compatibility-check.R` installs 0.3.5 and the candidate in isolated
+ libraries, then compares the public datasets, legacy calls, conditions, and
+ function signatures with `identical()`.
+- Independent tests cover bundle schema/checksum failures, cache corruption,
+ explicit version handling, `_ng` order/duplicate/missing behavior,
+ antimeridian and threshold searches, unavailable coordinates, and
+ authoritative/unmapped district behavior.
+- CI runs ordinary vignette-building `R CMD check --as-cran` on Windows,
+ macOS, Ubuntu release/devel/oldrel and runs the differential gate. The
+ Windows SHA fallback ignores non-portable `shasum.bat` wrappers.
+- A migration vignette documents unchanged legacy calls, explicit bundle
+ pinning, `_ng` calls, and recording both data version and bundle SHA in
+ research outputs.
+- `inst/CITATION` uses `bibentry()` rather than deprecated `citEntry()`.
# zipcodeR 0.3.5
-- Hotfix to address failing vignette to prevent package being archived by CRAN team.
+
+- Hotfix to address a failing vignette and prevent archival by CRAN.
# zipcodeR 0.3.4
-- Bug fix. Resolved an issue with ordering `zip_distance()` results (Pull request contributed by Nicholas X Lee).
+
+- Fixed ordering of `zip_distance()` results (contributed by Nicholas X Lee).
# zipcodeR 0.3.3
-- This update vectorizes the `zip_distance()` function to allow distance calculations between two vectors or columns of ZIP codes. The function now returns a data.frame of the resulting distance calculation.
-- `zip_distance()` now includes an additional argument, units, which allows selection between miles and meters for distance calculations.
+
+- Vectorized `zip_distance()` across two vectors or columns of ZIP codes.
+- Added the `units` argument to return miles or meters.
# zipcodeR 0.3.2
-- `zip_code_db` has been updated.
-- `download_zip_data()` has been refactored to make data updates more easily accessible and compare against existing data. Data is now directly downloaded from the upstream source and the existing data GitHub repository will no longer be updated.
-- `zip_distance()` has been updated to allow changing the type of distance calculation performed if specified via the lonlat argument.
-- Citation data is now included with package. If using `{zipcodeR}` in a publication, you can obtain citation info by running `citation("zipcodeR")`.
-- Misc updates to documentation and package reference info.
+
+- Updated `zip_code_db`.
+- Refactored `download_zip_data()` to download directly from upstream.
+- Added the `lonlat` distance-calculation argument.
+- Added package citation data.
# zipcodeR 0.3.1
-- Hotfix to address a problem for Mac users on the latest R release, the package no longer depends on `{udunits2}` for the `zip_distance()` and `search_radius()` functions.
+
+- Removed the `{udunits2}` dependency from distance and radius functions.
# zipcodeR 0.3.0
-- Added `search_radius()` function to allow searching for ZIP codes around a radius of lat / lon coordinates.
-- Added `zip_distance()` function for calculating the distance between ZIP codes using their centroids.
-- Added `geocode_zip()` function that returns the lat / lng centroid of a given ZIP code.
-- Added `normalize_zip()` function for normalizing messy ZIP code input (Contributed by Claus Wilke).
-- The `reverse_zipcode()` function has been updated to return a blank row for invalid ZIP codes with no matches in the zip code database.
-- The `search_` family of functions are now quieter.
+
+- Added `search_radius()`, `zip_distance()`, `geocode_zip()`, and
+ `normalize_zip()`.
+- Updated `reverse_zipcode()` to return a blank row for unmatched ZIP codes.
+- Made the `search_` family quieter.
# zipcodeR 0.2.0
-- `search_county()` function now allows for approximate matching of county names using agrep (Andre Mikulec)
-- `search_state()` is now vectorized and will accept a vector of state abbreviations
-- `search_tz()` is now vectorized and will accept a vector of timezones
-- `zip_code_db` has been updated to use latest upstream data
-- Added `reverse_zipcode()` function for obtaining metadata about a given ZIP code.
-- Added `search_cd()` function for searching ZIP codes contained within a given congressional district.
-- Added `is_zcta()` function for testing whether a given ZIP code is a ZIP code tabulation area (ZCTA).
-- Added `search_fips()` function for searching ZIP codes by state and county FIPS codes.
-- Added `get_cd()` and `search_cd()` functions for relating ZIP codes to congressional districts
-- Added the first vignette, "Introduction to zipcodeR"
+
+- Added approximate county matching, vectorized state/timezone searches,
+ reverse lookup, congressional-district helpers, ZCTA testing, and FIPS
+ searches.
+- Updated the ZIP database and added the introductory vignette.
# zipcodeR 0.1.0
-Initial public release, first version accepted by CRAN.
+
+- Initial CRAN release.
diff --git a/R/data.r b/R/data.r
index 753c4cb..7682ec1 100644
--- a/R/data.r
+++ b/R/data.r
@@ -2,7 +2,11 @@
#'
#' A dataset containing the relationships between ZIP code tabulation areas (ZCTA) and Census Tracts. This contains selected variables from the official crosswalk file.
#'
-#' @format A data frame with 148897 rows and 4 variables:
+#' This is the exact dataset distributed by zipcodeR 0.3.5. It is intentionally
+#' frozen so legacy calls remain reproducible; newer versioned relationships
+#' are available only through an explicit data bundle and the `_ng` API.
+#'
+#' @format A data frame with 148897 rows and 3 variables:
#' \describe{
#' \item{ZCTA5}{2010 ZIP Code Tabulation Area}
#' \item{TRACT}{2010 Census Tract Code}
@@ -14,10 +18,15 @@
#'
#' A dataset containing detailed information for U.S. ZIP codes
#'
+#' This is the exact 41,877-row database distributed by zipcodeR 0.3.5,
+#' including its historical values and limitations. It is intentionally frozen
+#' for research reproducibility. Use an explicit data bundle with `_ng`
+#' functions for corrected behavior and a pinned source manifest.
+#'
#' @format A data frame with 41877 rows and 24 variables:
#' \describe{
#' \item{zipcode}{5 digit U.S. ZIP code}
-#' \item{zipcode_type}{2010 State FIPS Code}
+#' \item{zipcode_type}{ZIP category such as Standard, PO Box, or Unique}
#' \item{major_city}{Major city serving the ZIP code}
#' \item{post_office_city}{City of post office serving the ZIP code}
#' \item{common_city_list}{List of common cities represented by the ZIP code}
@@ -47,6 +56,11 @@
#'
#' A dataset containing mappings between ZIP codes and congressional districts
#'
+#' This is the exact pre-2020 HUD-USPS-derived mapping shipped in zipcodeR
+#' 0.3.5. It is frozen for legacy reproducibility and should not be interpreted
+#' as a current redistricting vintage. Current authoritative ZCTA relationships
+#' are available in explicit data bundles.
+#'
#' @format A data frame with 45914 rows and 2 variables:
#' \describe{
#' \item{ZIP}{5 digit U.S. ZIP code}
diff --git a/R/data_bundle.R b/R/data_bundle.R
new file mode 100644
index 0000000..39ac826
--- /dev/null
+++ b/R/data_bundle.R
@@ -0,0 +1,462 @@
+#' Read a versioned zipcodeR data bundle
+#'
+#' Reads and validates a previously downloaded zipcodeR data bundle. Bundles
+#' are explicit, immutable inputs for the next-generation (`_ng`) API; reading
+#' one never changes the datasets used by the legacy API.
+#'
+#' @param path Path to a bundle `.rds` file.
+#' @param sha256 Optional expected SHA256 checksum. Supplying the checksum is
+#' strongly recommended when the file did not come from
+#' [download_zip_data_bundle()].
+#' @return An object of class `zipcodeR_data_bundle`.
+#' @export
+read_zip_data_bundle <- function(path, sha256 = NULL) {
+ if (!is.character(path) || length(path) != 1L || is.na(path) || !nzchar(path)) {
+ stop("`path` must be one non-empty file path.", call. = FALSE)
+ }
+ if (!file.exists(path)) {
+ stop("Data bundle does not exist: ", path, call. = FALSE)
+ }
+ if (!is.null(sha256)) {
+ validate_sha256(sha256, "sha256")
+ got <- file_sha256(path)
+ if (!identical(got, tolower(sha256))) {
+ stop(
+ "Checksum verification failed for data bundle.\n expected: ",
+ tolower(sha256), "\n got: ", got,
+ call. = FALSE
+ )
+ }
+ } else {
+ got <- file_sha256(path)
+ }
+
+ bundle <- readRDS(path)
+ bundle <- validate_zip_data_bundle(bundle)
+ attr(bundle, "bundle_sha256") <- got
+ attr(bundle, "bundle_path") <- normalizePath(path, mustWork = TRUE)
+ bundle
+}
+
+#' Download a versioned zipcodeR data bundle
+#'
+#' Downloads one explicitly requested data release, verifies its checksum, and
+#' caches it in the user's data directory. The function never resolves aliases
+#' such as `"latest"`, and no lookup function downloads data implicitly.
+#'
+#' @param version Exact registered data version, for example `"2026.08"`.
+#' @param force Re-download and replace a verified cached copy.
+#' @return A validated `zipcodeR_data_bundle`.
+#' @export
+download_zip_data_bundle <- function(version, force = FALSE) {
+ meta <- registered_zip_data_bundle(version)
+ cache_dir <- zipcodeR_user_data_dir()
+ download_zip_data_bundle_from(meta, force = force, cache_dir = cache_dir)
+}
+
+#' Report zipcodeR data-version metadata
+#'
+#' With no argument, reports the immutable data bundled with the legacy API.
+#' Given a downloaded data bundle, reports that bundle's metadata and verified
+#' checksum.
+#'
+#' @param x `NULL` for the legacy bundled data, or a
+#' `zipcodeR_data_bundle` object.
+#' @return A named metadata list.
+#' @export
+zip_data_version <- function(x = NULL) {
+ if (is.null(x)) {
+ return(legacy_zip_data_meta())
+ }
+ x <- validate_zip_data_bundle(x)
+ out <- x$metadata
+ out$bundle_sha256 <- attr(x, "bundle_sha256", exact = TRUE)
+ out
+}
+
+#' Inspect the provenance supplied with a zipcodeR data bundle
+#'
+#' @param bundle A `zipcodeR_data_bundle`.
+#' @param dataset Optional dataset name (`"zip_code_db"`,
+#' `"zcta_crosswalk"`, or `"zip_to_cd"`).
+#' @param key Optional record key (ZIP or ZCTA) to select.
+#' @return A provenance data frame combining source/method records with
+#' record-level quality status and reasons.
+#' @export
+zip_data_provenance <- function(bundle, dataset = NULL, key = NULL) {
+ bundle <- validate_zip_data_bundle(bundle)
+ provenance_columns <- c(
+ "dataset", "key", "field", "source_id", "method", "quality", "note"
+ )
+ quality <- data.frame(
+ dataset = bundle$quality$dataset,
+ key = bundle$quality$key,
+ field = bundle$quality$field,
+ source_id = NA_character_,
+ method = bundle$quality$status,
+ quality = bundle$quality$status,
+ note = bundle$quality$reason,
+ stringsAsFactors = FALSE
+ )
+ provenance <- rbind(
+ bundle$provenance[, provenance_columns, drop = FALSE],
+ quality[, provenance_columns, drop = FALSE]
+ )
+ if (!is.null(dataset)) {
+ if (!is.character(dataset) || length(dataset) != 1L || is.na(dataset)) {
+ stop("`dataset` must be one dataset name.", call. = FALSE)
+ }
+ allowed <- c("zip_code_db", "zcta_crosswalk", "zip_to_cd")
+ if (!dataset %in% allowed) {
+ stop(
+ "Unknown `dataset`; choose one of: ", paste(allowed, collapse = ", "),
+ call. = FALSE
+ )
+ }
+ provenance <- provenance[provenance$dataset == dataset, , drop = FALSE]
+ }
+ if (!is.null(key)) {
+ key <- as.character(key)
+ provenance <- provenance[provenance$key %in% c("*", key), , drop = FALSE]
+ }
+ provenance
+}
+
+#' @noRd
+legacy_zip_data_meta <- function() {
+ list(
+ data_version = "legacy-0.3.5",
+ package_version = "0.3.5",
+ zip_code_db_rows = 41877L,
+ zcta_crosswalk_rows = 148897L,
+ zip_to_cd_rows = 45914L,
+ sources = list(
+ zip_code_db = "uszipcode-project 0.2.6-db-file (2021-06-08)",
+ zcta_crosswalk = "U.S. Census 2010 ZCTA-to-tract relationship file",
+ zip_to_cd = "pre-2020 HUD-USPS congressional-district crosswalk"
+ ),
+ compatibility_contract = "Exact zipcodeR 0.3.5 defaults"
+ )
+}
+
+#' @noRd
+registered_zip_data_bundle <- function(version) {
+ if (!is.character(version) || length(version) != 1L || is.na(version) ||
+ !nzchar(version)) {
+ stop("`version` must be one explicit data version.", call. = FALSE)
+ }
+ if (version %in% c("latest", "current", "stable")) {
+ stop(
+ "Aliases such as '", version, "' are not supported; pin an exact data version.",
+ call. = FALSE
+ )
+ }
+ registry <- zip_data_bundle_registry()
+ if (!version %in% names(registry)) {
+ stop(
+ "Unknown zipcodeR data version '", version, "'. Registered versions: ",
+ paste(names(registry), collapse = ", "),
+ call. = FALSE
+ )
+ }
+ registry[[version]]
+}
+
+# Registry entries are updated only after the corresponding public release
+# asset has been downloaded and independently checksum-verified.
+#' @noRd
+zip_data_bundle_registry <- function() {
+ list(
+ "2026.08" = list(
+ version = "2026.08",
+ asset = "zipcodeR-data-2026.08.rds",
+ url = paste0(
+ "https://github.com/gavinrozzi/zipcodeR/releases/download/",
+ "data-2026.08/zipcodeR-data-2026.08.rds"
+ ),
+ sha256 = "9059026c159a4d1311ad9c61ba5193a6123299503efb5d867b8303c9d23627e4"
+ )
+ )
+}
+
+#' @noRd
+download_zip_data_bundle_from <- function(meta, force, cache_dir) {
+ if (!is.logical(force) || length(force) != 1L || is.na(force)) {
+ stop("`force` must be TRUE or FALSE.", call. = FALSE)
+ }
+ validate_bundle_registry_entry(meta)
+ dir.create(cache_dir, recursive = TRUE, showWarnings = FALSE)
+ dest <- file.path(cache_dir, meta$asset)
+
+ if (file.exists(dest) && !force) {
+ if (identical(file_sha256(dest), meta$sha256)) {
+ bundle <- read_zip_data_bundle(dest, sha256 = meta$sha256)
+ return(validate_registered_bundle_version(bundle, meta))
+ }
+ message("zipcodeR: cached bundle failed checksum verification; re-downloading")
+ }
+ if (!zipcodeR_has_internet()) {
+ stop("No internet connection. The requested data bundle was not cached.", call. = FALSE)
+ }
+
+ tmp <- tempfile(paste0(basename(dest), "-"), tmpdir = cache_dir)
+ on.exit(unlink(tmp), add = TRUE)
+ old_timeout <- options(timeout = max(600, getOption("timeout")))
+ on.exit(options(old_timeout), add = TRUE)
+ tryCatch(
+ download_zip_data_file(meta$url, tmp),
+ error = function(e) {
+ stop("Data bundle download failed: ", conditionMessage(e), call. = FALSE)
+ }
+ )
+ got <- file_sha256(tmp)
+ if (!identical(got, meta$sha256)) {
+ stop(
+ "Checksum verification failed for downloaded data bundle.\n expected: ",
+ meta$sha256, "\n got: ", got,
+ call. = FALSE
+ )
+ }
+ if (!file.rename(tmp, dest) && !file.copy(tmp, dest, overwrite = TRUE)) {
+ stop("Failed to move the verified bundle into the cache: ", dest, call. = FALSE)
+ }
+ bundle <- read_zip_data_bundle(dest, sha256 = meta$sha256)
+ validate_registered_bundle_version(bundle, meta)
+}
+
+# Small wrappers keep download failure, interruption, and offline behavior
+# testable without allowing tests to contact the network.
+#' @noRd
+zipcodeR_has_internet <- function() curl::has_internet()
+
+#' @noRd
+download_zip_data_file <- function(url, path) {
+ utils::download.file(url, path, mode = "wb", quiet = TRUE)
+}
+
+#' @noRd
+validate_registered_bundle_version <- function(bundle, meta) {
+ if (!identical(bundle$metadata$data_version, meta$version)) {
+ stop(
+ "Registered bundle version mismatch. Expected '", meta$version,
+ "' but the bundle reports '", bundle$metadata$data_version, "'.",
+ call. = FALSE
+ )
+ }
+ bundle
+}
+
+#' @noRd
+validate_bundle_registry_entry <- function(meta) {
+ required <- c("version", "asset", "url", "sha256")
+ if (!is.list(meta) || !all(required %in% names(meta))) {
+ stop("Malformed zipcodeR data-bundle registry entry.", call. = FALSE)
+ }
+ validate_sha256(meta$sha256, "registered sha256")
+ invisible(meta)
+}
+
+#' @noRd
+validate_sha256 <- function(x, label) {
+ if (!is.character(x) || length(x) != 1L || is.na(x) ||
+ !grepl("^[0-9a-fA-F]{64}$", x)) {
+ stop("`", label, "` must be a 64-character SHA256 value.", call. = FALSE)
+ }
+ invisible(tolower(x))
+}
+
+# Validate a published manifest against its bundle. This is intentionally
+# internal: callers pin bundle bytes through download_zip_data_bundle() or
+# read_zip_data_bundle(), while release automation uses this additional gate
+# before a registry entry is enabled.
+#' @noRd
+validate_zip_data_release_manifest <- function(manifest_path, bundle_path,
+ manifest_sha256 = NULL) {
+ for (item in c("manifest_path", "bundle_path")) {
+ value <- get(item)
+ if (!is.character(value) || length(value) != 1L || is.na(value) ||
+ !nzchar(value) || !file.exists(value)) {
+ stop("`", item, "` must name one existing file.", call. = FALSE)
+ }
+ }
+ if (!is.null(manifest_sha256)) {
+ validate_sha256(manifest_sha256, "manifest_sha256")
+ got_manifest <- file_sha256(manifest_path)
+ if (!identical(got_manifest, tolower(manifest_sha256))) {
+ stop("Manifest checksum verification failed.", call. = FALSE)
+ }
+ }
+
+ manifest <- tryCatch(
+ jsonlite::fromJSON(manifest_path, simplifyVector = FALSE),
+ error = function(e) {
+ stop("Invalid data-release manifest: ", conditionMessage(e), call. = FALSE)
+ }
+ )
+ required <- c(
+ "data_version", "asset", "asset_sha256", "rows", "schemas",
+ "sources", "output_hashes", "pipeline_commit", "r_version",
+ "dependency_lock_sha256"
+ )
+ if (!is.list(manifest) || !all(required %in% names(manifest))) {
+ stop(
+ "Invalid data-release manifest: missing required fields: ",
+ paste(setdiff(required, names(manifest)), collapse = ", "),
+ call. = FALSE
+ )
+ }
+ validate_sha256(manifest$asset_sha256, "manifest asset_sha256")
+ if (!identical(basename(bundle_path), manifest$asset)) {
+ stop("Manifest asset name does not match the bundle file.", call. = FALSE)
+ }
+ bundle <- read_zip_data_bundle(bundle_path, sha256 = manifest$asset_sha256)
+ if (!identical(bundle$metadata$data_version, manifest$data_version)) {
+ stop("Manifest data version does not match the bundle.", call. = FALSE)
+ }
+
+ expected_rows <- c(
+ zip_code_db = nrow(bundle$zip_code_db),
+ zcta_crosswalk = nrow(bundle$zcta_crosswalk),
+ zip_to_cd = nrow(bundle$zip_to_cd)
+ )
+ manifest_rows <- unlist(manifest$rows[ names(expected_rows) ], use.names = TRUE)
+ if (!identical(as.numeric(manifest_rows), as.numeric(expected_rows))) {
+ stop("Manifest row counts do not match the bundle.", call. = FALSE)
+ }
+
+ expected_schemas <- list(
+ zip_code_db = vapply(bundle$zip_code_db, function(x) class(x)[1], character(1)),
+ zcta_crosswalk = vapply(bundle$zcta_crosswalk, function(x) class(x)[1], character(1)),
+ zip_to_cd = vapply(bundle$zip_to_cd, function(x) class(x)[1], character(1))
+ )
+ for (dataset in names(expected_schemas)) {
+ schema <- manifest$schemas[[dataset]]
+ if (!is.list(schema) || !length(schema) ||
+ !all(vapply(schema, function(x) {
+ is.list(x) && all(c("name", "type") %in% names(x))
+ }, logical(1)))) {
+ stop("Manifest schema is malformed for `", dataset, "`.", call. = FALSE)
+ }
+ schema_names <- vapply(schema, function(x) x$name, character(1))
+ schema_types <- vapply(schema, function(x) x$type, character(1))
+ if (!identical(schema_names, names(expected_schemas[[dataset]])) ||
+ !identical(schema_types, unname(expected_schemas[[dataset]]))) {
+ stop("Manifest schema does not match `", dataset, "`.", call. = FALSE)
+ }
+ }
+ bundle_hashes <- bundle$metadata$output_hashes
+ if (!is.list(bundle_hashes) ||
+ !identical(unlist(manifest$output_hashes), unlist(bundle_hashes))) {
+ stop("Manifest canonical output hashes do not match the bundle.", call. = FALSE)
+ }
+ invisible(bundle)
+}
+
+#' @noRd
+validate_zip_data_bundle <- function(bundle) {
+ required <- c(
+ "zip_code_db", "zcta_crosswalk", "zip_to_cd", "metadata",
+ "provenance", "quality"
+ )
+ if (!is.list(bundle) || !all(required %in% names(bundle))) {
+ stop(
+ "Invalid zipcodeR data bundle: required entries are ",
+ paste(required, collapse = ", "), ".",
+ call. = FALSE
+ )
+ }
+ if (!is.data.frame(bundle$zip_code_db) ||
+ !identical(names(bundle$zip_code_db), legacy_zip_code_db_columns())) {
+ stop("Invalid `zip_code_db` schema in data bundle.", call. = FALSE)
+ }
+ if (anyDuplicated(bundle$zip_code_db$zipcode)) {
+ stop("Invalid data bundle: `zip_code_db$zipcode` is not unique.", call. = FALSE)
+ }
+ if (!is.character(bundle$zip_code_db$zipcode) ||
+ !all(grepl("^[0-9]{5}$", bundle$zip_code_db$zipcode))) {
+ stop("Invalid data bundle: ZIP identifiers must be five characters.", call. = FALSE)
+ }
+ if (!is.numeric(bundle$zip_code_db$lat) || !is.numeric(bundle$zip_code_db$lng) ||
+ any(xor(is.na(bundle$zip_code_db$lat), is.na(bundle$zip_code_db$lng))) ||
+ any(abs(bundle$zip_code_db$lat) > 90, na.rm = TRUE) ||
+ any(abs(bundle$zip_code_db$lng) > 180, na.rm = TRUE)) {
+ stop("Invalid data bundle: malformed coordinate values.", call. = FALSE)
+ }
+ if (!is.data.frame(bundle$zcta_crosswalk) ||
+ !identical(names(bundle$zcta_crosswalk), c("ZCTA5", "TRACT", "GEOID"))) {
+ stop("Invalid `zcta_crosswalk` schema in data bundle.", call. = FALSE)
+ }
+ if (!is.character(bundle$zcta_crosswalk$ZCTA5) ||
+ !is.character(bundle$zcta_crosswalk$TRACT) ||
+ !is.character(bundle$zcta_crosswalk$GEOID)) {
+ stop("Invalid `zcta_crosswalk` types in data bundle.", call. = FALSE)
+ }
+ if (!all(grepl("^[0-9]{5}$", bundle$zcta_crosswalk$ZCTA5)) ||
+ !all(grepl("^[0-9]{6}$", bundle$zcta_crosswalk$TRACT)) ||
+ !all(grepl("^[0-9]{11}$", bundle$zcta_crosswalk$GEOID)) ||
+ anyDuplicated(bundle$zcta_crosswalk)) {
+ stop("Invalid identifier values in `zcta_crosswalk`.", call. = FALSE)
+ }
+ if (!is.data.frame(bundle$zip_to_cd) ||
+ !identical(names(bundle$zip_to_cd), c("ZIP", "CD"))) {
+ stop("Invalid `zip_to_cd` schema in data bundle.", call. = FALSE)
+ }
+ if (!is.character(bundle$zip_to_cd$ZIP) || !is.character(bundle$zip_to_cd$CD) ||
+ !all(grepl("^[0-9]{5}$", bundle$zip_to_cd$ZIP)) ||
+ !all(grepl("^[0-9]{4}$", bundle$zip_to_cd$CD)) ||
+ anyDuplicated(bundle$zip_to_cd)) {
+ stop("Invalid identifier values in `zip_to_cd`.", call. = FALSE)
+ }
+ if (!is.list(bundle$metadata) ||
+ !is.character(bundle$metadata$data_version %||% NULL) ||
+ length(bundle$metadata$data_version) != 1L ||
+ is.na(bundle$metadata$data_version) ||
+ !nzchar(bundle$metadata$data_version)) {
+ stop("Invalid data bundle: metadata must contain `data_version`.", call. = FALSE)
+ }
+ provenance_columns <- c(
+ "dataset", "key", "field", "source_id", "method", "quality", "note"
+ )
+ if (!is.data.frame(bundle$provenance) ||
+ !all(provenance_columns %in% names(bundle$provenance))) {
+ stop("Invalid data bundle: malformed provenance table.", call. = FALSE)
+ }
+ quality_columns <- c("dataset", "key", "field", "status", "reason")
+ if (!is.data.frame(bundle$quality) ||
+ !all(quality_columns %in% names(bundle$quality))) {
+ stop("Invalid data bundle: malformed quality table.", call. = FALSE)
+ }
+ class(bundle) <- unique(c("zipcodeR_data_bundle", class(bundle)))
+ bundle
+}
+
+#' @noRd
+legacy_zip_code_db_columns <- function() {
+ c(
+ "zipcode", "zipcode_type", "major_city", "post_office_city",
+ "common_city_list", "county", "state", "lat", "lng", "timezone",
+ "radius_in_miles", "area_code_list", "population", "population_density",
+ "land_area_in_sqmi", "water_area_in_sqmi", "housing_units",
+ "occupied_housing_units", "median_home_value", "median_household_income",
+ "bounds_west", "bounds_east", "bounds_north", "bounds_south"
+ )
+}
+
+#' @noRd
+zipcodeR_user_data_dir <- function() {
+ if (getRversion() >= "4.0.0") {
+ return(tools::R_user_dir("zipcodeR", "data"))
+ }
+ if (.Platform$OS.type == "windows") {
+ root <- Sys.getenv("LOCALAPPDATA", unset = path.expand("~/AppData/Local"))
+ return(file.path(root, "zipcodeR", "data"))
+ }
+ root <- Sys.getenv("XDG_DATA_HOME", unset = path.expand("~/.local/share"))
+ file.path(root, "zipcodeR")
+}
+
+# Base R 3.5 does not provide the null-coalescing helper used internally.
+#' @noRd
+`%||%` <- function(x, y) {
+ if (is.null(x)) y else x
+}
diff --git a/R/data_version.R b/R/data_version.R
new file mode 100644
index 0000000..71a9f5f
--- /dev/null
+++ b/R/data_version.R
@@ -0,0 +1,203 @@
+#' Download the comprehensive ZIP code database
+#'
+#' The bundled \code{zip_code_db} is the lightweight ("simple") dataset. A
+#' much larger companion database with detailed ACS demographic profiles per
+#' ZIP code (the "comprehensive" database, ~450 MB SQLite) is published as a
+#' checksum-pinned asset of a zipcodeR data release rather than shipped in the
+#' package. Its public URL, checksum, and SQLite integrity have passed a
+#' clean-machine smoke test.
+#'
+#' This function downloads that database, verifies its SHA256 checksum,
+#' and caches it under \code{tools::R_user_dir("zipcodeR", "data")}; later
+#' calls return the cached path immediately. It never downloads without being
+#' called explicitly. For offline use, copy the file to that directory
+#' yourself (the expected file name is the asset name from the data release).
+#'
+#' @param force If TRUE, re-download even if a verified copy is cached.
+#' @return Invisibly, the path to the downloaded SQLite database. Query it
+#' with DBI/RSQLite, e.g.
+#' \code{DBI::dbConnect(RSQLite::SQLite(), download_comprehensive_data())}.
+#' @examples
+#' \dontrun{
+#' path <- download_comprehensive_data()
+#' }
+#' @export
+download_comprehensive_data <- function(force = FALSE) {
+ meta <- comprehensive_data_registry()
+ if (!isTRUE(meta$published)) {
+ stop(
+ "The comprehensive data asset is not registered for public download. ",
+ "Use only a release whose URL and checksum have been verified.",
+ call. = FALSE
+ )
+ }
+ # Verify SHA256 capability BEFORE any download: discovering its absence
+ # after a ~450 MB transfer would discard the download on every attempt
+ ensure_sha256_available()
+ cache_dir <- zipcodeR_user_data_dir()
+ dest <- file.path(cache_dir, meta$asset)
+
+ if (file.exists(dest) && !force) {
+ if (identical(file_sha256(dest), meta$sha256)) {
+ message("zipcodeR: using cached comprehensive database at ", dest)
+ return(invisible(dest))
+ }
+ message("zipcodeR: cached file failed checksum verification; re-downloading")
+ }
+
+ if (!curl::has_internet()) {
+ stop("No internet connection. Please connect to the internet and try again.")
+ }
+
+ url <- sprintf(
+ "https://github.com/gavinrozzi/zipcodeR/releases/download/%s/%s",
+ meta$release_tag, meta$asset
+ )
+ dir.create(cache_dir, showWarnings = FALSE, recursive = TRUE)
+ message(
+ "zipcodeR: downloading the comprehensive database (~450 MB) from the ",
+ meta$release_tag, " data release.\nThis is a one-time download cached in ",
+ cache_dir
+ )
+ tmp <- tempfile(paste0(meta$asset, "-"), tmpdir = cache_dir)
+ on.exit(unlink(tmp), add = TRUE)
+ # R's default download timeout (60s) is far too short for ~450 MB;
+ # raise it for this call only
+ old_timeout <- options(timeout = max(3600, getOption("timeout")))
+ on.exit(options(old_timeout), add = TRUE)
+ tryCatch(
+ utils::download.file(url, tmp, mode = "wb"),
+ error = function(e) {
+ stop(
+ "Download failed: ", conditionMessage(e),
+ "\nIf this is a 404, the '", meta$release_tag, "' data release may ",
+ "not have been published on GitHub yet - see ",
+ "https://github.com/gavinrozzi/zipcodeR/releases",
+ call. = FALSE
+ )
+ }
+ )
+
+ got <- file_sha256(tmp)
+ if (!identical(got, meta$sha256)) {
+ stop(
+ "Checksum verification failed for the downloaded database.\n expected: ",
+ meta$sha256, "\n got: ", got,
+ "\nThe download may be corrupted or tampered with; not keeping it."
+ )
+ }
+ # Prefer an atomic rename; it cannot overwrite an existing file on
+ # Windows, so fall back to an overwriting copy. The old cache is only
+ # replaced, never deleted ahead of a successful move, and leftover tmp
+ # cleanup is handled by on.exit (a failed cleanup is not a failure).
+ if (!file.rename(tmp, dest)) {
+ if (!file.copy(tmp, dest, overwrite = TRUE)) {
+ stop("Failed to move the verified download into place at ", dest)
+ }
+ }
+ message("zipcodeR: download complete and verified: ", dest)
+ invisible(dest)
+}
+
+# Updated only after a public-release smoke test succeeds.
+#' @noRd
+comprehensive_data_registry <- function() {
+ list(
+ published = TRUE,
+ release_tag = "data-2026.08",
+ asset = "comprehensive_db.sqlite",
+ sha256 = "d85ed4e25884bc27bdd339d57dd9e2d1763531d4c050acb7a05a3d5aca90668d"
+ )
+}
+
+# Stop with an informative error when no SHA256 mechanism exists, so the
+# capability is established before any large download
+#' @noRd
+ensure_sha256_available <- function() {
+ ok <- exists("sha256sum", envir = asNamespace("tools"), inherits = FALSE) ||
+ requireNamespace("openssl", quietly = TRUE) ||
+ length(sha256_system_binary()) == 1L
+ if (!ok) {
+ stop(
+ "No SHA256 tool available to verify the download. Install the ",
+ "'openssl' package (install.packages(\"openssl\")), upgrade to ",
+ "R >= 4.5, or ensure a shasum/sha256sum binary is on the PATH."
+ )
+ }
+ invisible(TRUE)
+}
+
+# SHA256 of a file without adding a package dependency: prefer
+# tools::sha256sum (R >= 4.5), fall back to the openssl package if installed,
+# then to the system shasum/sha256sum binaries. Every backend must return a
+# PLAIN character string: the results are compared with identical(), which is
+# FALSE for an equal-looking string carrying attributes.
+#' @noRd
+file_sha256 <- function(path) {
+ if (exists("sha256sum", envir = asNamespace("tools"), inherits = FALSE)) {
+ return(unname(tools::sha256sum(path)))
+ }
+ if (requireNamespace("openssl", quietly = TRUE)) {
+ return(sha256_openssl(path))
+ }
+ sha256_system(path)
+}
+
+#' @noRd
+sha256_openssl <- function(path) {
+ con <- file(path, "rb")
+ on.exit(close(con), add = TRUE)
+ # as.character() on an openssl hash keeps its c("hash", "sha256") class.
+ # Without unclass() the result is never identical() to the expected plain
+ # string, so on R < 4.5 with openssl installed every checksum comparison
+ # would fail - rejecting a correct download as corrupted.
+ unclass(as.character(openssl::sha256(con)))
+}
+
+#' @noRd
+sha256_system <- function(path) {
+ bin <- sha256_system_binary()
+ if (!length(bin)) {
+ stop("No SHA256 tool available: need R >= 4.5, the openssl package, or a system shasum/sha256sum binary.")
+ }
+ # shasum (incl. shasum.exe / shasum.bat on Windows) defaults to SHA-1 and
+ # needs the algorithm flag; sha256sum does not
+ is_shasum <- grepl("^shasum", basename(bin), ignore.case = TRUE)
+ native_path <- normalizePath(
+ path,
+ winslash = if (.Platform$OS.type == "windows") "\\" else "/",
+ mustWork = TRUE
+ )
+ args <- if (is_shasum) c("-a", "256", shQuote(native_path)) else shQuote(native_path)
+ out <- suppressWarnings(system2(bin, args, stdout = TRUE, stderr = TRUE))
+ status <- attr(out, "status")
+ if (!is.null(status) && status != 0L) {
+ stop(
+ "SHA256 command failed with status ", status, ": ",
+ paste(out, collapse = "\n"),
+ call. = FALSE
+ )
+ }
+ if (!length(out)) {
+ stop("SHA256 command produced no output: ", bin, call. = FALSE)
+ }
+ out <- strsplit(out[[1]], "[[:space:]]+")[[1]][1]
+ if (!grepl("^[0-9a-f]{64}$", out)) {
+ stop("Unexpected output from ", bin, " while computing SHA256: ", out)
+ }
+ out
+}
+
+# Strawberry Perl exposes shasum.bat on Windows, but invoking that wrapper via
+# system2() is not portable and caused the package's Windows check failure.
+# R >= 4.5 and openssl remain cross-platform; system fallback on Windows is
+# used only for a real sha256sum executable.
+#' @noRd
+sha256_system_binary <- function(os_type = .Platform$OS.type,
+ bins = Sys.which(c("sha256sum", "shasum"))) {
+ bins <- unname(bins[nzchar(bins)])
+ if (identical(os_type, "windows")) {
+ bins <- bins[!grepl("\\.(bat|cmd)$", bins, ignore.case = TRUE)]
+ }
+ if (!length(bins)) character(0) else bins[[1]]
+}
diff --git a/R/distance.R b/R/distance.R
new file mode 100644
index 0000000..eeb70e3
--- /dev/null
+++ b/R/distance.R
@@ -0,0 +1,26 @@
+#' Great-circle distance between coordinate pairs (haversine formula)
+#'
+#' Vectorized over all four arguments (recycled as needed). Returns the
+#' spherical (haversine) distance in meters using the mean Earth radius
+#' of 6,371,008.8 m. Distances involving NA coordinates return NA.
+#'
+#' This helper is used only by the explicit `_ng` API. Legacy functions retain
+#' their WGS84 geodesic calculation through `raster::pointDistance()` because
+#' haversine results are not scientifically identical.
+#'
+#' @param lat_a,lng_a coordinates of the first point(s), decimal degrees
+#' @param lat_b,lng_b coordinates of the second point(s), decimal degrees
+#' @return numeric vector of distances in meters
+#' @noRd
+haversine_distance <- function(lat_a, lng_a, lat_b, lng_b) {
+ earth_radius_m <- 6371008.8
+ to_rad <- pi / 180
+
+ dlat <- (lat_b - lat_a) * to_rad
+ dlng <- (lng_b - lng_a) * to_rad
+
+ h <- sin(dlat / 2)^2 +
+ cos(lat_a * to_rad) * cos(lat_b * to_rad) * sin(dlng / 2)^2
+
+ 2 * earth_radius_m * asin(pmin(1, sqrt(h)))
+}
diff --git a/R/download_data.r b/R/download_data.r
index d5766b0..ab938df 100644
--- a/R/download_data.r
+++ b/R/download_data.r
@@ -1,4 +1,12 @@
-#' Download updated data files needed for library functionality to the package's data directory. To be implemented for future updates.
+#' Legacy in-place data downloader
+#'
+#' This function is retained without behavioral changes for reproducibility
+#' with zipcodeR 0.3.5. It attempts to modify files inside an installed package
+#' and resolves an upstream "latest" release, so it is unsuitable for a pinned
+#' research workflow. New work should use [download_zip_data_bundle()] or
+#' [read_zip_data_bundle()] and pass the resulting object explicitly to the
+#' `_ng` API. No runtime warning is emitted because warnings are part of the
+#' frozen legacy contract.
#'
#' @param force Boolean, if set to TRUE will force overwrite existing data files with new version
#' @return Data files needed for package functionality, stored in data directory of package install
@@ -10,7 +18,7 @@
#' @importFrom DBI dbGetQuery
#' @importFrom jsonlite fromJSON
#' @importFrom httr http_error
-#' @importFrom dplyr `%>%`
+#' @importFrom dplyr %>%
#' @importFrom dplyr filter
#' @importFrom curl has_internet
#' @export
diff --git a/R/ng_functions.R b/R/ng_functions.R
new file mode 100644
index 0000000..a684f73
--- /dev/null
+++ b/R/ng_functions.R
@@ -0,0 +1,461 @@
+# Next-generation lookup API. Every data-dependent function requires an
+# explicit, validated bundle as its first argument. The legacy API remains in
+# zip_lookups.r and zip_helper_functions.R without behavioral changes.
+
+#' Search a state using an explicit data bundle
+#' @param data A `zipcodeR_data_bundle`.
+#' @param state_abb Two-letter state abbreviation(s).
+#' @return A tibble grouped in query order; repeated states repeat their rows.
+#' @export
+search_state_ng <- function(data, state_abb) {
+ db <- ng_zip_db(data)
+ state_abb <- toupper(as.character(state_abb))
+ validate_ng_query_vector(state_abb, "state_abb", "^[A-Z]{2}$")
+ matched <- lapply(state_abb, function(state) which(db$state == state))
+ missing_values <- unique(state_abb[!lengths(matched)])
+ if (length(missing_values)) {
+ warning(
+ "No ZIP codes found for state(s): ",
+ paste(missing_values, collapse = ", "),
+ call. = FALSE
+ )
+ }
+ result <- db[unlist(matched, use.names = FALSE), , drop = FALSE]
+ if (!nrow(result)) stop("No ZIP codes found for requested state(s).", call. = FALSE)
+ stamp_ng_result(dplyr::as_tibble(result), data)
+}
+
+#' Search a county using an explicit data bundle
+#' @param data A `zipcodeR_data_bundle`.
+#' @param county_name County name.
+#' @param state_abb Two-letter state abbreviation.
+#' @param ... Set `similar = TRUE` and optionally `max.distance` for approximate matching.
+#' @return A tibble from the selected data vintage.
+#' @export
+search_county_ng <- function(data, county_name, state_abb, ...) {
+ db <- ng_zip_db(data)
+ assert_scalar(county_name, "county_name")
+ assert_scalar(state_abb, "state_abb")
+ county_name <- as.character(county_name)
+ state_abb <- toupper(as.character(state_abb))
+ if (!grepl("^[A-Z]{2}$", state_abb)) {
+ stop("`state_abb` must contain exactly two letters.", call. = FALSE)
+ }
+ dots <- rlang::list2(...)
+ unknown_dots <- setdiff(names(dots), c("similar", "max.distance"))
+ if (length(unknown_dots)) {
+ stop("Unknown argument(s): ", paste(unknown_dots, collapse = ", "), call. = FALSE)
+ }
+ similar <- dots$similar %||% FALSE
+ if (!is.logical(similar) || length(similar) != 1L || is.na(similar)) {
+ stop("`similar` must be TRUE or FALSE.", call. = FALSE)
+ }
+ state_rows <- db[db$state == state_abb, , drop = FALSE]
+ if (similar) {
+ max_distance <- dots$max.distance %||% 0.1
+ if (!is.numeric(max_distance) || length(max_distance) != 1L ||
+ is.na(max_distance) || !is.finite(max_distance) || max_distance < 0) {
+ stop("`max.distance` must be one non-negative number.", call. = FALSE)
+ }
+ matched <- agrep(
+ county_name, state_rows$county, ignore.case = TRUE, value = TRUE,
+ max.distance = max_distance
+ )
+ result <- state_rows[state_rows$county %in% matched, , drop = FALSE]
+ } else {
+ requested <- sub("\\s+County$", "", county_name, ignore.case = TRUE)
+ result <- state_rows[
+ tolower(state_rows$county) == tolower(paste(requested, "County")),
+ , drop = FALSE
+ ]
+ }
+ if (!nrow(result)) {
+ stop("No ZIP codes found for county: ", county_name, ", ", state_abb)
+ }
+ stamp_ng_result(dplyr::as_tibble(result), data)
+}
+
+#' Reverse-geocode ZIP codes using an explicit data bundle
+#' @param data A `zipcodeR_data_bundle`.
+#' @param zip_code Five-digit ZIP code(s).
+#' @return One row per input, in input order, with duplicates preserved. The
+#' result adds `state_fips` (two digits) and `county_fips` (the full
+#' five-digit county identifier) for the database's predominant county.
+#' @export
+reverse_zipcode_ng <- function(data, zip_code) {
+ db <- ng_zip_db(data)
+ zip_code <- as.character(zip_code)
+ matched <- match(zip_code, db$zipcode)
+ missing_values <- unique(zip_code[is.na(matched)])
+ if (length(missing_values)) {
+ warning(
+ "No data found for ZIP code(s): ", paste(missing_values, collapse = ", "),
+ call. = FALSE
+ )
+ }
+ result <- db[matched, , drop = FALSE]
+ result$zipcode <- zip_code
+ fips_key <- paste(result$state, result$county, sep = "\r")
+ fips_reference_key <- paste(fips_codes$state, fips_codes$county, sep = "\r")
+ fips_match <- match(fips_key, fips_reference_key)
+ result$state_fips <- fips_codes$state_code[fips_match]
+ result$county_fips <- ifelse(
+ is.na(fips_match),
+ NA_character_,
+ paste0(fips_codes$state_code[fips_match], fips_codes$county_code[fips_match])
+ )
+ stamp_ng_result(dplyr::as_tibble(result), data)
+}
+
+#' Search a city using an explicit data bundle
+#' @param data A `zipcodeR_data_bundle`.
+#' @param city_name City name.
+#' @param state_abb Two-letter state abbreviation.
+#' @return A tibble from the selected data vintage.
+#' @export
+search_city_ng <- function(data, city_name, state_abb) {
+ db <- ng_zip_db(data)
+ assert_scalar(city_name, "city_name")
+ assert_scalar(state_abb, "state_abb")
+ city_name <- as.character(city_name)
+ state_abb <- toupper(as.character(state_abb))
+ if (!grepl("^[A-Z]{2}$", state_abb)) {
+ stop("`state_abb` must contain exactly two letters.", call. = FALSE)
+ }
+ result <- db[
+ db$state == state_abb & tolower(db$major_city) == tolower(city_name),
+ , drop = FALSE
+ ]
+ if (!nrow(result)) stop("No ZIP codes found for city: ", city_name, ", ", state_abb)
+ stamp_ng_result(dplyr::as_tibble(result), data)
+}
+
+#' Search a timezone using an explicit data bundle
+#' @param data A `zipcodeR_data_bundle`.
+#' @param tz Timezone value(s).
+#' @return A tibble grouped in query order; repeated timezones repeat their rows.
+#' @export
+search_tz_ng <- function(data, tz) {
+ db <- ng_zip_db(data)
+ tz <- as.character(tz)
+ validate_ng_query_vector(tz, "tz")
+ matched <- lapply(tz, function(zone) which(db$timezone == zone))
+ missing_values <- unique(tz[!lengths(matched)])
+ if (length(missing_values)) {
+ warning(
+ "No ZIP codes found for timezone(s): ",
+ paste(missing_values, collapse = ", "),
+ call. = FALSE
+ )
+ }
+ result <- db[unlist(matched, use.names = FALSE), , drop = FALSE]
+ if (!nrow(result)) stop("No ZIP codes found for requested timezone(s).", call. = FALSE)
+ stamp_ng_result(dplyr::as_tibble(result), data)
+}
+
+#' Search FIPS codes using an explicit data bundle
+#' @param data A `zipcodeR_data_bundle`.
+#' @param state_fips One state FIPS code.
+#' @param county_fips Optional one- to three-digit county FIPS code.
+#' @return A tibble from the selected data vintage.
+#' @export
+search_fips_ng <- function(data, state_fips, county_fips) {
+ db <- ng_zip_db(data)
+ assert_scalar(state_fips, "state_fips")
+ state_fips <- as.character(state_fips)
+ if (!grepl("^[0-9]{1,2}$", state_fips)) {
+ stop("`state_fips` must contain one or two digits.", call. = FALSE)
+ }
+ state_fips <- sprintf("%02d", as.integer(state_fips))
+ if (missing(county_fips)) {
+ fips_result <- fips_codes[fips_codes$state_code == state_fips, , drop = FALSE]
+ } else {
+ assert_scalar(county_fips, "county_fips")
+ county_fips <- as.character(county_fips)
+ if (!grepl("^[0-9]{1,3}$", county_fips)) {
+ stop("`county_fips` must contain one to three digits.", call. = FALSE)
+ }
+ county_fips <- sprintf("%03d", as.integer(county_fips))
+ fips_result <- fips_codes[
+ fips_codes$state_code == state_fips & fips_codes$county_code == county_fips,
+ , drop = FALSE
+ ]
+ }
+ if (!nrow(fips_result)) stop("No matching FIPS code found.")
+ result <- db[db$state == fips_result$state[[1]], , drop = FALSE]
+ if (!missing(county_fips)) {
+ result <- result[result$county == fips_result$county[[1]], , drop = FALSE]
+ }
+ stamp_ng_result(dplyr::as_tibble(result), data)
+}
+
+#' Get Census tracts using an explicit data bundle
+#' @param data A `zipcodeR_data_bundle`.
+#' @param zip_code One five-digit ZIP or ZCTA code.
+#' @return A tract crosswalk tibble.
+#' @export
+get_tracts_ng <- function(data, zip_code) {
+ data <- validate_zip_data_bundle(data)
+ assert_scalar(zip_code, "zip_code")
+ zip_code <- as.character(zip_code)
+ if (!grepl("^[0-9]{5}$", zip_code)) {
+ stop("`zip_code` must contain exactly 5 digits.", call. = FALSE)
+ }
+ result <- data$zcta_crosswalk[data$zcta_crosswalk$ZCTA5 == zip_code, , drop = FALSE]
+ if (!nrow(result)) stop("No Census tracts found for ZIP code ", zip_code)
+ stamp_ng_result(dplyr::as_tibble(result), data)
+}
+
+#' Get congressional districts using an explicit data bundle
+#' @param data A `zipcodeR_data_bundle`.
+#' @param zip_code One five-digit ZIP code.
+#' @return A named list with parallel `state_fips` and `district` vectors.
+#' @export
+get_cd_ng <- function(data, zip_code) {
+ data <- validate_zip_data_bundle(data)
+ assert_scalar(zip_code, "zip_code")
+ zip_code <- as.character(zip_code)
+ if (!grepl("^[0-9]{5}$", zip_code)) {
+ stop("`zip_code` must contain exactly 5 digits.", call. = FALSE)
+ }
+ matched <- data$zip_to_cd[data$zip_to_cd$ZIP == zip_code, , drop = FALSE]
+ if (!nrow(matched)) {
+ reason <- data$quality[
+ data$quality$dataset == "zip_to_cd" & data$quality$key == zip_code,
+ "reason", drop = TRUE
+ ]
+ warning(
+ "No authoritative congressional district found for ZIP code ", zip_code,
+ if (length(reason)) paste0(": ", reason[[1]]) else ".",
+ call. = FALSE
+ )
+ }
+ state_code <- substr(matched$CD, 1, 2)
+ district <- substr(matched$CD, 3, 4)
+ state_abb <- fips_codes$state[match(state_code, fips_codes$state_code)]
+ stamp_ng_result(list(state_fips = state_abb, district = district), data)
+}
+
+#' Search a congressional district using an explicit data bundle
+#' @param data A `zipcodeR_data_bundle`.
+#' @param state_fips_code One state FIPS code.
+#' @param congressional_district One two-digit district code.
+#' @return A ZIP-to-district tibble.
+#' @export
+search_cd_ng <- function(data, state_fips_code, congressional_district) {
+ data <- validate_zip_data_bundle(data)
+ assert_scalar(state_fips_code, "state_fips_code")
+ assert_scalar(congressional_district, "congressional_district")
+ state_fips_code <- as.character(state_fips_code)
+ congressional_district <- as.character(congressional_district)
+ if (!grepl("^[0-9]{1,2}$", state_fips_code)) {
+ stop("`state_fips_code` must contain one or two digits.", call. = FALSE)
+ }
+ if (!grepl("^[0-9]{1,2}$", congressional_district)) {
+ stop("`congressional_district` must contain one or two digits.", call. = FALSE)
+ }
+ state_fips_code <- sprintf("%02d", as.integer(state_fips_code))
+ congressional_district <- sprintf("%02d", as.integer(congressional_district))
+ district_codes <- congressional_district
+ if (congressional_district %in% c("00", "98")) district_codes <- c("00", "98")
+ codes <- paste0(state_fips_code, district_codes)
+ result <- data$zip_to_cd[data$zip_to_cd$CD %in% codes, , drop = FALSE]
+ if (!nrow(result)) stop("No ZIP codes found for congressional district: ", congressional_district)
+ result$CD <- NULL
+ result$state_fips <- state_fips_code
+ result$congressional_district <- congressional_district
+ stamp_ng_result(dplyr::as_tibble(result), data)
+}
+
+#' Test ZCTA membership using an explicit data bundle
+#' @param data A `zipcodeR_data_bundle`.
+#' @param zip_code ZIP code(s).
+#' @return A logical vector.
+#' @export
+is_zcta_ng <- function(data, zip_code) {
+ data <- validate_zip_data_bundle(data)
+ stamp_ng_result(as.character(zip_code) %in% data$zcta_crosswalk$ZCTA5, data)
+}
+
+#' Geocode ZIP codes using an explicit data bundle
+#' @param data A `zipcodeR_data_bundle`.
+#' @param zip_code ZIP code(s).
+#' @return One coordinate row per input, preserving order and duplicates.
+#' @export
+geocode_zip_ng <- function(data, zip_code) {
+ db <- ng_zip_db(data)
+ zip_code <- as.character(zip_code)
+ matched <- match(zip_code, db$zipcode)
+ if (!length(matched)) {
+ return(stamp_ng_result(dplyr::tibble(
+ zipcode = character(), lat = numeric(), lng = numeric()
+ ), data))
+ }
+ if (all(is.na(matched))) {
+ stop("No results found for ZIP code ", paste(zip_code, collapse = ", "))
+ }
+ if (anyNA(matched)) {
+ warning(
+ "No results found for ZIP code(s): ",
+ paste(unique(zip_code[is.na(matched)]), collapse = ", "),
+ call. = FALSE
+ )
+ }
+ result <- dplyr::tibble(
+ zipcode = zip_code,
+ lat = db$lat[matched],
+ lng = db$lng[matched]
+ )
+ stamp_ng_result(result, data)
+}
+
+#' Search within a radius using an explicit data bundle
+#' @param data A `zipcodeR_data_bundle`.
+#' @param lat,lng Query coordinates.
+#' @param radius Radius in miles.
+#' @return A tibble of ZIP codes and haversine distances.
+#' @export
+search_radius_ng <- function(data, lat, lng, radius = 1) {
+ db <- ng_zip_db(data)
+ validate_point_radius(lat, lng, radius)
+ keep <- !is.na(db$lat) & !is.na(db$lng)
+ lat_delta <- radius / 69 * 1.05
+ edge_lat <- min(abs(lat) + lat_delta, 90)
+ if (edge_lat < 89) {
+ lng_delta <- radius / (69.172 * cos(edge_lat * pi / 180)) * 1.05
+ keep <- keep & db$lat >= lat - lat_delta & db$lat <= lat + lat_delta
+ if (lng_delta < 180) {
+ lng_diff <- abs(((db$lng - lng + 180) %% 360) - 180)
+ keep <- keep & lng_diff <= lng_delta
+ }
+ }
+ distance <- haversine_distance(db$lat[keep], db$lng[keep], lat, lng) * 0.000621371
+ result <- data.frame(zipcode = db$zipcode[keep], distance = distance)
+ result <- result[result$distance <= radius, , drop = FALSE]
+ result <- result[order(result$distance), , drop = FALSE]
+ if (!nrow(result)) warning("No ZIP codes found within the requested radius.", call. = FALSE)
+ stamp_ng_result(dplyr::as_tibble(result), data)
+}
+
+#' Normalize ZIP codes with corrected boundary behavior
+#' @param zipcode Character or numeric ZIP values.
+#' @return Normalized five-character ZIP values.
+#' @export
+normalize_zip_ng <- function(zipcode) {
+ capture_group <- function(values, regex) {
+ matches <- regmatches(values, regexec(regex, values))
+ vapply(matches, function(x) if (length(x) >= 2L) x[[2]] else NA_character_, character(1))
+ }
+ if (is.character(zipcode)) {
+ zipcode <- ifelse(
+ grepl("^\\s*(\\d+)-.*", zipcode),
+ capture_group(zipcode, "^\\s*(\\d+)-.*"), zipcode
+ )
+ missing_values <- is.na(zipcode)
+ zipcode <- ifelse(
+ nchar(zipcode) > 5L,
+ capture_group(zipcode, "(.*)\\d\\d\\d\\d"), zipcode
+ )
+ zipcode <- ifelse(nchar(zipcode) < 5L, sprintf("%05i", as.numeric(zipcode)), zipcode)
+ zipcode[missing_values] <- NA_character_
+ return(zipcode)
+ }
+ if (!is.numeric(zipcode)) stop("input must be character or numeric")
+ zipcode <- ifelse(zipcode >= 100000, floor(zipcode / 10000), zipcode)
+ missing_values <- is.na(zipcode)
+ zipcode <- sprintf("%05i", as.numeric(zipcode))
+ zipcode[missing_values] <- NA_character_
+ zipcode
+}
+
+#' Calculate ZIP-to-ZIP distance using an explicit data bundle
+#' @param data A `zipcodeR_data_bundle`.
+#' @param zipcode_a,zipcode_b ZIP vectors.
+#' @param lonlat Use great-circle haversine distance; `FALSE` uses a planar
+#' equirectangular approximation.
+#' @param units `"miles"` or `"meters"`.
+#' @return A data frame of paired ZIP codes and distances.
+#' @export
+zip_distance_ng <- function(data, zipcode_a, zipcode_b, lonlat = TRUE,
+ units = c("miles", "meters")) {
+ db <- ng_zip_db(data)
+ units <- match.arg(units)
+ if (!is.logical(lonlat) || length(lonlat) != 1L || is.na(lonlat)) {
+ stop("`lonlat` must be TRUE or FALSE.", call. = FALSE)
+ }
+ zipcode_a <- as.character(zipcode_a)
+ zipcode_b <- as.character(zipcode_b)
+ if (length(zipcode_a) != length(zipcode_b)) {
+ n <- max(length(zipcode_a), length(zipcode_b))
+ m <- min(length(zipcode_a), length(zipcode_b))
+ if (!m || n %% m) stop("ZIP vectors have incompatible lengths.")
+ zipcode_a <- rep_len(zipcode_a, n)
+ zipcode_b <- rep_len(zipcode_b, n)
+ }
+ a <- match(zipcode_a, db$zipcode)
+ b <- match(zipcode_b, db$zipcode)
+ lat_a <- db$lat[a]; lng_a <- db$lng[a]
+ lat_b <- db$lat[b]; lng_b <- db$lng[b]
+ if (isTRUE(lonlat)) {
+ distance <- haversine_distance(lat_a, lng_a, lat_b, lng_b)
+ } else {
+ meters_per_degree <- 6371008.8 * pi / 180
+ dlng <- ((lng_b - lng_a + 180) %% 360) - 180
+ dx <- dlng * cos((lat_a + lat_b) / 2 * pi / 180)
+ dy <- lat_b - lat_a
+ distance <- sqrt(dx^2 + dy^2) * meters_per_degree
+ }
+ if (units == "miles") distance <- distance * 0.000621371
+ result <- data.frame(zipcode_a, zipcode_b, distance = round(distance, 2))
+ stamp_ng_result(result, data)
+}
+
+#' @noRd
+ng_zip_db <- function(data) {
+ validate_zip_data_bundle(data)$zip_code_db
+}
+
+#' @noRd
+stamp_ng_result <- function(x, data) {
+ data <- validate_zip_data_bundle(data)
+ attr(x, "zipcodeR_data_version") <- data$metadata$data_version
+ checksum <- attr(data, "bundle_sha256", exact = TRUE)
+ if (!is.null(checksum)) attr(x, "zipcodeR_bundle_sha256") <- checksum
+ x
+}
+
+#' @noRd
+assert_scalar <- function(x, name) {
+ if (length(x) != 1L || is.na(x)) {
+ stop("`", name, "` must be one non-missing value.", call. = FALSE)
+ }
+ invisible(x)
+}
+
+#' @noRd
+validate_point_radius <- function(lat, lng, radius) {
+ if (!is.numeric(lat) || length(lat) != 1L || is.na(lat) ||
+ !is.finite(lat) || abs(lat) > 90) {
+ stop("`lat` must be one latitude between -90 and 90.", call. = FALSE)
+ }
+ if (!is.numeric(lng) || length(lng) != 1L || is.na(lng) ||
+ !is.finite(lng) || abs(lng) > 180) {
+ stop("`lng` must be one longitude between -180 and 180.", call. = FALSE)
+ }
+ if (!is.numeric(radius) || length(radius) != 1L || is.na(radius) ||
+ !is.finite(radius) || radius < 0) {
+ stop("`radius` must be one non-negative number of miles.", call. = FALSE)
+ }
+ invisible(TRUE)
+}
+
+#' @noRd
+validate_ng_query_vector <- function(x, name, pattern = NULL) {
+ if (!length(x) || anyNA(x) || any(!nzchar(x))) {
+ stop("`", name, "` must contain one or more non-missing values.", call. = FALSE)
+ }
+ if (!is.null(pattern) && any(!grepl(pattern, x))) {
+ stop("`", name, "` contains an invalid value.", call. = FALSE)
+ }
+ invisible(x)
+}
diff --git a/R/sysdata.rda b/R/sysdata.rda
index 14f32a2..542f901 100644
Binary files a/R/sysdata.rda and b/R/sysdata.rda differ
diff --git a/R/zip_helper_functions.R b/R/zip_helper_functions.R
index 5047229..8f4af65 100644
--- a/R/zip_helper_functions.R
+++ b/R/zip_helper_functions.R
@@ -75,7 +75,6 @@ normalize_zip <- function(zipcode) {
#' @examples
#' zip_distance("08731", "08901")
#'
-#' @importFrom raster pointDistance
#' @export
zip_distance <- function(zipcode_a, zipcode_b, lonlat = TRUE, units = "miles") {
zipcode_a <- as.character(zipcode_a)
diff --git a/R/zip_lookups.r b/R/zip_lookups.r
index d21050a..33b7fd1 100644
--- a/R/zip_lookups.r
+++ b/R/zip_lookups.r
@@ -242,7 +242,6 @@ get_tracts <- function(zip_code) {
#' get_cd("90210")
#' @importFrom dplyr %>%
#' @importFrom rlang .data
-#' @import tidycensus
#' @export
get_cd <- function(zip_code) {
# Get state FIPS codes data from tidycensus library
@@ -350,7 +349,6 @@ geocode_zip <- function(zip_code) {
#' \dontrun{
#' search_radius(39.9, -74.3, 10)
#' }
-#' @importFrom raster pointDistance
#' @export
search_radius <- function(lat, lng, radius = 1) {
@@ -359,9 +357,45 @@ search_radius <- function(lat, lng, radius = 1) {
zip_data <- zip_code_db %>%
dplyr::filter(lat != "NA")
- # Calculate the distance between all points and the provided coordinate pair
- for (i in seq_len(nrow(zip_data))) {
- zip_data$distance[i] <- raster::pointDistance(c(lng, lat), c(zip_data$lng[i], zip_data$lat[i]), lonlat = TRUE)
+ # For ordinary scalar queries, discard points that cannot possibly be in
+ # the result before calling the unchanged WGS84 calculation. Sixty statute
+ # miles per degree is deliberately broader than the WGS84 minimum, and the
+ # longitude test uses the most poleward edge of the latitude band. This is
+ # therefore a conservative prefilter, not a distance approximation.
+ # Unusual/invalid inputs bypass it so their historical conditions remain
+ # byte-for-byte comparable with 0.3.5.
+ ordinary_query <- is.numeric(lat) && length(lat) == 1L && !is.na(lat) &&
+ is.finite(lat) && abs(lat) <= 90 &&
+ is.numeric(lng) && length(lng) == 1L && !is.na(lng) &&
+ is.finite(lng) && abs(lng) <= 180 &&
+ is.numeric(radius) && length(radius) == 1L && !is.na(radius) &&
+ is.finite(radius) && radius >= 0
+ if (ordinary_query) {
+ lat_delta <- radius / 60
+ keep <- zip_data$lat >= lat - lat_delta & zip_data$lat <= lat + lat_delta
+ edge_lat <- abs(lat) + lat_delta
+ if (edge_lat < 89) {
+ lng_delta <- radius / (60 * cos(edge_lat * pi / 180))
+ if (lng_delta < 180) {
+ lng_difference <- abs(((zip_data$lng - lng + 180) %% 360) - 180)
+ keep <- keep & lng_difference <= lng_delta
+ }
+ }
+ zip_data <- zip_data[keep, , drop = FALSE]
+ }
+
+ # Calculate the same pairwise WGS84 geodesic distances as the historical
+ # row-by-row implementation, but in one vectorized raster call. Repeating
+ # the query point keeps pointDistance() in pairwise mode and preserves the
+ # exact legacy algorithm, values, row order, and NA handling.
+ if (nrow(zip_data)) {
+ query_points <- matrix(c(lng, lat), nrow = nrow(zip_data), ncol = 2, byrow = TRUE)
+ zip_points <- cbind(zip_data$lng, zip_data$lat)
+ zip_data$distance <- raster::pointDistance(
+ query_points, zip_points, lonlat = TRUE
+ )
+ } else {
+ zip_data$distance <- numeric(0)
}
# Convert meters to miles for distance measurement
diff --git a/R/zipcodeR-package.R b/R/zipcodeR-package.R
new file mode 100644
index 0000000..9b38cf6
--- /dev/null
+++ b/R/zipcodeR-package.R
@@ -0,0 +1,23 @@
+#' zipcodeR: reproducible U.S. ZIP-code analysis
+#'
+#' zipcodeR provides two deliberately separate interfaces.
+#'
+#' @section Recommended interface for new analyses:
+#' Use functions ending in `_ng` with an explicitly selected data bundle. Start
+#' with [download_zip_data_bundle()] for a registered immutable version or
+#' [read_zip_data_bundle()] for a local checksum-pinned file. Passing the bundle
+#' as the first argument makes the chosen data vintage visible in every call.
+#' `_ng` never resolves an implicit `latest` version or downloads during a
+#' lookup.
+#'
+#' @section Historical compatibility interface:
+#' Unsuffixed functions and the datasets in `data/` retain the exact zipcodeR
+#' 0.3.5 contract for existing scripts and research reproduction. This includes
+#' historical data, scientific algorithms, conditions, ordering, and known edge
+#' cases. Use [zip_data_version()] to record which contract an analysis used.
+#'
+#' @docType package
+#' @name zipcodeR-package
+#' @aliases zipcodeR
+#' @keywords internal
+"_PACKAGE"
diff --git a/README.Rmd b/README.Rmd
index 86e9c4b..088a7fa 100644
--- a/README.Rmd
+++ b/README.Rmd
@@ -12,9 +12,10 @@ knitr::opts_chunk$set(
out.width = "100%"
)
library(tibble)
+devtools::load_all(quiet = TRUE)
```
-# zipcodeR
+# zipcodeR
[](https://github.com/gavinrozzi/zipcodeR/actions)
@@ -28,7 +29,7 @@ library(tibble)
`{zipcodeR}` is an R package that makes working with ZIP codes in R easier. It provides data on all U.S. ZIP codes using multiple open data sources, making it easier for social science researchers and data scientists to work with ZIP code-level data in data science projects using R.
-The latest update to `{zipcodeR}` includes new functions for [searching ZIP codes at various geographic levels & geocoding.](https://gavinrozzi.github.io/zipcodeR/articles/geographic.html)
+The latest update to `{zipcodeR}` includes new functions for [searching ZIP codes at various geographic levels & geocoding.](https://zipcoder.39n.io/articles/geographic.html)
## Installation
@@ -45,6 +46,45 @@ And the development version from [GitHub](https://github.com/) with:
devtools::install_github("gavinrozzi/zipcodeR")
```
+## Reproducibility and data vintages
+
+For new analyses, the recommended interface is the `_ng` API with a named,
+checksum-verified modern data bundle. The suffix is a deliberate signal that
+the caller has chosen the newer data and corrected behavior. A project still
+pins one exact bundle version; `_ng` never means "automatically use whatever
+is newest."
+
+Version 0.4.0 also keeps every existing function and all three bundled
+datasets identical to 0.3.5. Existing calls therefore keep their historical
+results, including known edge-case behavior:
+
+```{r}
+zip_distance("08731", "08901")
+get_cd("08731")
+zip_data_version()
+```
+
+Start a new project by downloading the exact registered bundle version, or
+reading a checksum-pinned file for an offline workflow, then pass the bundle
+explicitly to `_ng` functions:
+
+```{r, eval=FALSE}
+bundle <- download_zip_data_bundle("2026.08")
+# Offline alternative:
+# bundle <- read_zip_data_bundle(
+# "zipcodeR-data-2026.08.rds",
+# sha256 = "SHA256_FROM_THE_RELEASE_MANIFEST"
+# )
+
+zip_distance_ng(bundle, "08731", "08901")
+get_cd_ng(bundle, "08731")
+zip_data_version(bundle)
+```
+
+No lookup downloads data, selects a `latest` version, or changes a global
+option. Record `zip_data_version(bundle)` and its `bundle_sha256` field with
+research outputs. See the "Legacy and next-generation data" vignette.
+
## Citing `{zipcodeR}` in Publications
If you use `{zipcodeR}` in a publication, please cite the following [journal article](https://www.sciencedirect.com/science/article/pii/S2665963821000373/).
@@ -67,72 +107,33 @@ abstract = {The United States Postal Service (USPS) assigns unique identifiers f
}
```
-## Examples
+## Recommended workflow for new analyses
-
-```{r}
-# Load zipcodeR into R
+```{r, eval=FALSE}
library(zipcodeR)
+bundle <- download_zip_data_bundle("2026.08")
+
+search_state_ng(bundle, "NJ")
+zip_distance_ng(bundle, "08901", "08731")
+geocode_zip_ng(bundle, "08901")
+reverse_zipcode_ng(bundle, "08901")
+search_county_ng(bundle, "Ocean", "NJ")
+search_city_ng(bundle, "Jersey City", "NJ")
+search_tz_ng(bundle, "Eastern")
+get_tracts_ng(bundle, "08731")
+
+# Save this beside the analysis outputs.
+saveRDS(zip_data_version(bundle), "zipcodeR-data-version.rds")
```
-
-### Find all ZIP codes for a state
-
-```{r}
-search_state('NJ')
-```
-
-### Calculate the distance between two ZIP codes in miles
-
-```{r}
-zip_distance('08901','08731')
-```
-
-
-### Calculate the distance between vectors of ZIP codes
-
-```{r}
-zip_codes <- tribble(~zip_a, ~zip_b,
-"08731", "08901",
-"08734", "08005")
-
-zip_distance(zip_codes$zip_a,zip_codes$zip_b)
-```
-
-### Geocode a ZIP code to get its centroid
-```{r}
-geocode_zip('08901')
-```
-
-### Get data about a ZIP code
-```{r}
-reverse_zipcode('08901')
-```
-
-### Find all ZIP codes for a county
-```{r}
-search_county('Ocean','NJ')
-```
-
-
-### Find all ZIP codes for a city
-```{r}
-search_city('Jersey City','NJ')
-```
-
-### Find all ZIP codes for a timezone
-```{r}
-search_tz('Eastern')
-```
-
-### Get all Census tracts for a given ZIP code
-```{r}
-get_tracts('08731')
-```
+For existing scripts, keep using the unsuffixed functions. They intentionally
+continue to use the historical 0.3.5 data and behavior, so upgrading zipcodeR
+does not rewrite a prior result. New code should use `_ng` only after choosing
+and recording the modern bundle version it intends to analyze.
## Documentation
-Documentation for the current release [is available here.](https://gavinrozzi.github.io/zipcodeR/)
-See the [reference section](https://gavinrozzi.github.io/zipcodeR/reference/) for full details on how to use each of the functions provided by zipcodeR.
+Documentation for the current release [is available here.](https://zipcoder.39n.io/)
+See the [reference section](https://zipcoder.39n.io/reference/) for full details on how to use each of the functions provided by zipcodeR.
## Data Sources
This project was inspired by the excellent [uszipcode](https://uszipcode.readthedocs.io/index.html) library for Python and utilizes the same backend database released by its author under the MIT license. This project also incorporates open data from the U.S. Census Bureau and Department of Housing & Urban Development.
diff --git a/README.md b/README.md
index b55480e..45db22f 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
-# zipcodeR
+# zipcodeR
@@ -25,7 +25,7 @@ using R.
The latest update to `{zipcodeR}` includes new functions for [searching
ZIP codes at various geographic levels &
-geocoding.](https://gavinrozzi.github.io/zipcodeR/articles/geographic.html)
+geocoding.](https://zipcoder.39n.io/articles/geographic.html)
## Installation
@@ -43,6 +43,81 @@ And the development version from [GitHub](https://github.com/) with:
devtools::install_github("gavinrozzi/zipcodeR")
```
+## Reproducibility and data vintages
+
+For new analyses, the recommended interface is the `_ng` API with a
+named, checksum-verified modern data bundle. The suffix is a deliberate
+signal that the caller has chosen the newer data and corrected behavior.
+A project still pins one exact bundle version; `_ng` never means
+“automatically use whatever is newest.”
+
+Version 0.4.0 also keeps every existing function and all three bundled
+datasets identical to 0.3.5. Existing calls therefore keep their
+historical results, including known edge-case behavior:
+
+``` r
+zip_distance("08731", "08901")
+#> zipcode_a zipcode_b distance
+#> 1 08731 08901 40.7
+get_cd("08731")
+#> $state_fips
+#> [1] "NJ"
+#>
+#> $district
+#> [1] "03"
+zip_data_version()
+#> $data_version
+#> [1] "legacy-0.3.5"
+#>
+#> $package_version
+#> [1] "0.3.5"
+#>
+#> $zip_code_db_rows
+#> [1] 41877
+#>
+#> $zcta_crosswalk_rows
+#> [1] 148897
+#>
+#> $zip_to_cd_rows
+#> [1] 45914
+#>
+#> $sources
+#> $sources$zip_code_db
+#> [1] "uszipcode-project 0.2.6-db-file (2021-06-08)"
+#>
+#> $sources$zcta_crosswalk
+#> [1] "U.S. Census 2010 ZCTA-to-tract relationship file"
+#>
+#> $sources$zip_to_cd
+#> [1] "pre-2020 HUD-USPS congressional-district crosswalk"
+#>
+#>
+#> $compatibility_contract
+#> [1] "Exact zipcodeR 0.3.5 defaults"
+```
+
+Start a new project by downloading the exact registered bundle version,
+or reading a checksum-pinned file for an offline workflow, then pass the
+bundle explicitly to `_ng` functions:
+
+``` r
+bundle <- download_zip_data_bundle("2026.08")
+# Offline alternative:
+# bundle <- read_zip_data_bundle(
+# "zipcodeR-data-2026.08.rds",
+# sha256 = "SHA256_FROM_THE_RELEASE_MANIFEST"
+# )
+
+zip_distance_ng(bundle, "08731", "08901")
+get_cd_ng(bundle, "08731")
+zip_data_version(bundle)
+```
+
+No lookup downloads data, selects a `latest` version, or changes a
+global option. Record `zip_data_version(bundle)` and its `bundle_sha256`
+field with research outputs. See the “Legacy and next-generation data”
+vignette.
+
## Citing `{zipcodeR}` in Publications
If you use `{zipcodeR}` in a publication, please cite the following
@@ -67,190 +142,37 @@ abstract = {The United States Postal Service (USPS) assigns unique identifiers f
}
```
-## Examples
+## Recommended workflow for new analyses
``` r
-# Load zipcodeR into R
library(zipcodeR)
-#> Warning: package 'zipcodeR' was built under R version 4.3.2
-```
-
-### Find all ZIP codes for a state
-
-``` r
-search_state('NJ')
-#> # A tibble: 732 × 24
-#> zipcode zipcode_type major_city post_office_city common_city_list county
-#>
-#> 1 07001 Standard Avenel Avenel, NJ Middl…
-#> 2 07002 Standard Bayonne Bayonne, NJ Hudso…
-#> 3 07003 Standard Bloomfield Bloomfield, NJ Essex…
-#> 4 07004 Standard Fairfield Fairfield, NJ Essex…
-#> 5 07005 Standard Boonton Boonton, NJ Morri…
-#> 6 07006 Standard Caldwell Caldwell, NJ Essex…
-#> 7 07007 PO Box Caldwell Essex…
-#> 8 07008 Standard Carteret Carteret, NJ Middl…
-#> 9 07009 Standard Cedar Grove Cedar Grove, NJ Essex…
-#> 10 07010 Standard Cliffside Park Cliffside Park, … Berge…
-#> # ℹ 722 more rows
-#> # ℹ 18 more variables: state , lat , lng , timezone ,
-#> # radius_in_miles , area_code_list , population ,
-#> # population_density , land_area_in_sqmi ,
-#> # water_area_in_sqmi , housing_units ,
-#> # occupied_housing_units , median_home_value ,
-#> # median_household_income , bounds_west , bounds_east , …
-```
-
-### Calculate the distance between two ZIP codes in miles
-
-``` r
-zip_distance('08901','08731')
-#> zipcode_a zipcode_b distance
-#> 1 08901 08731 40.7
-```
-
-### Calculate the distance between vectors of ZIP codes
-
-``` r
-zip_codes <- tribble(~zip_a, ~zip_b,
-"08731", "08901",
-"08734", "08005")
-
-zip_distance(zip_codes$zip_a,zip_codes$zip_b)
-#> zipcode_a zipcode_b distance
-#> 1 08731 08901 40.70
-#> 2 08734 08005 8.06
+bundle <- download_zip_data_bundle("2026.08")
+
+search_state_ng(bundle, "NJ")
+zip_distance_ng(bundle, "08901", "08731")
+geocode_zip_ng(bundle, "08901")
+reverse_zipcode_ng(bundle, "08901")
+search_county_ng(bundle, "Ocean", "NJ")
+search_city_ng(bundle, "Jersey City", "NJ")
+search_tz_ng(bundle, "Eastern")
+get_tracts_ng(bundle, "08731")
+
+# Save this beside the analysis outputs.
+saveRDS(zip_data_version(bundle), "zipcodeR-data-version.rds")
```
-### Geocode a ZIP code to get its centroid
-
-``` r
-geocode_zip('08901')
-#> # A tibble: 1 × 3
-#> zipcode lat lng
-#>
-#> 1 08901 40.5 -74.4
-```
-
-### Get data about a ZIP code
-
-``` r
-reverse_zipcode('08901')
-#> # A tibble: 1 × 24
-#> zipcode zipcode_type major_city post_office_city common_city_list county state
-#>
-#> 1 08901 Standard New Bruns… New Brunswick, … Middl… NJ
-#> # ℹ 17 more variables: lat , lng , timezone ,
-#> # radius_in_miles , area_code_list , population ,
-#> # population_density , land_area_in_sqmi ,
-#> # water_area_in_sqmi , housing_units ,
-#> # occupied_housing_units , median_home_value ,
-#> # median_household_income , bounds_west , bounds_east ,
-#> # bounds_north , bounds_south
-```
-
-### Find all ZIP codes for a county
-
-``` r
-search_county('Ocean','NJ')
-#> # A tibble: 32 × 24
-#> zipcode zipcode_type major_city post_office_city common_city_list county
-#>
-#> 1 08005 Standard Barnegat Barnegat, NJ Ocean…
-#> 2 08006 PO Box Barnegat Light Barnegat Light, … Ocean…
-#> 3 08008 Standard Beach Haven Beach Haven, NJ Ocean…
-#> 4 08050 Standard Manahawkin Manahawkin, NJ Ocean…
-#> 5 08087 Standard Tuckerton Tuckerton, NJ Ocean…
-#> 6 08092 Standard West Creek West Creek, NJ Ocean…
-#> 7 08527 Standard Jackson Jackson, NJ Ocean…
-#> 8 08533 Standard New Egypt New Egypt, NJ Ocean…
-#> 9 08701 Standard Lakewood Lakewood, NJ Ocean…
-#> 10 08721 Standard Bayville Bayville, NJ Ocean…
-#> # ℹ 22 more rows
-#> # ℹ 18 more variables: state , lat , lng , timezone ,
-#> # radius_in_miles , area_code_list , population ,
-#> # population_density , land_area_in_sqmi ,
-#> # water_area_in_sqmi , housing_units ,
-#> # occupied_housing_units , median_home_value ,
-#> # median_household_income , bounds_west , bounds_east , …
-```
-
-### Find all ZIP codes for a city
-
-``` r
-search_city('Jersey City','NJ')
-#> # A tibble: 13 × 24
-#> zipcode zipcode_type major_city post_office_city common_city_list county
-#>
-#> 1 07097 Unique Jersey City Hudson Co…
-#> 2 07302 Standard Jersey City Jersey City, NJ Hudson Co…
-#> 3 07303 PO Box Jersey City Hudson Co…
-#> 4 07304 Standard Jersey City Jersey City, NJ Hudson Co…
-#> 5 07305 Standard Jersey City Jersey City, NJ Hudson Co…
-#> 6 07306 Standard Jersey City Jersey City, NJ Hudson Co…
-#> 7 07307 Standard Jersey City Jersey City, NJ Hudson Co…
-#> 8 07308 PO Box Jersey City Hudson Co…
-#> 9 07309 Standard Jersey City Hudson Co…
-#> 10 07310 Standard Jersey City Jersey City, NJ Hudson Co…
-#> 11 07311 Standard Jersey City Jersey City, NJ Hudson Co…
-#> 12 07395 Unique Jersey City Hudson Co…
-#> 13 07399 Unique Jersey City Hudson Co…
-#> # ℹ 18 more variables: state , lat , lng , timezone ,
-#> # radius_in_miles , area_code_list , population ,
-#> # population_density , land_area_in_sqmi ,
-#> # water_area_in_sqmi , housing_units ,
-#> # occupied_housing_units , median_home_value ,
-#> # median_household_income , bounds_west , bounds_east ,
-#> # bounds_north , bounds_south
-```
-
-### Find all ZIP codes for a timezone
-
-``` r
-search_tz('Eastern')
-#> # A tibble: 14,025 × 24
-#> zipcode zipcode_type major_city post_office_city common_city_list county
-#>
-#> 1 06001 Standard Avon Avon, CT Hartfo…
-#> 2 06002 Standard Bloomfield Bloomfield, CT Hartfo…
-#> 3 06010 Standard Bristol Bristol, CT Hartfo…
-#> 4 06013 Standard Burlington Burlington, CT Hartfo…
-#> 5 06016 Standard Broad Brook Broad Brook, CT Hartfo…
-#> 6 06018 Standard Canaan Canaan, CT Litchf…
-#> 7 06019 Standard Canton Canton, CT Hartfo…
-#> 8 06020 Standard Canton Center Canton Center, CT Hartfo…
-#> 9 06021 Standard Colebrook Colebrook, CT Litchf…
-#> 10 06022 Standard Collinsville Collinsville, CT Hartfo…
-#> # ℹ 14,015 more rows
-#> # ℹ 18 more variables: state , lat , lng , timezone ,
-#> # radius_in_miles , area_code_list , population ,
-#> # population_density , land_area_in_sqmi ,
-#> # water_area_in_sqmi , housing_units ,
-#> # occupied_housing_units , median_home_value ,
-#> # median_household_income , bounds_west , bounds_east , …
-```
-
-### Get all Census tracts for a given ZIP code
-
-``` r
-get_tracts('08731')
-#> # A tibble: 6 × 3
-#> ZCTA5 TRACT GEOID
-#>
-#> 1 08731 732001 34029732001
-#> 2 08731 732002 34029732002
-#> 3 08731 732101 34029732101
-#> 4 08731 732103 34029732103
-#> 5 08731 732104 34029732104
-#> 6 08731 733000 34029733000
-```
+For existing scripts, keep using the unsuffixed functions. They
+intentionally continue to use the historical 0.3.5 data and behavior, so
+upgrading zipcodeR does not rewrite a prior result. New code should use
+`_ng` only after choosing and recording the modern bundle version it
+intends to analyze.
## Documentation
Documentation for the current release [is available
-here.](https://gavinrozzi.github.io/zipcodeR/) See the [reference
-section](https://gavinrozzi.github.io/zipcodeR/reference/) for full
-details on how to use each of the functions provided by zipcodeR.
+here.](https://zipcoder.39n.io/) See the [reference
+section](https://zipcoder.39n.io/reference/) for full details on how to
+use each of the functions provided by zipcodeR.
## Data Sources
diff --git a/ROADMAP.md b/ROADMAP.md
new file mode 100644
index 0000000..20ca792
--- /dev/null
+++ b/ROADMAP.md
@@ -0,0 +1,69 @@
+# zipcodeR Roadmap
+
+zipcodeR is a research dependency. Reproducibility therefore takes precedence
+over correcting historical behavior under an existing function or dataset
+name.
+
+## 0.4.0 — compatibility-first infrastructure
+
+The release is mergeable only after every required gate below passes.
+
+| Item | Status |
+|---|---|
+| Restore the three exact 0.3.5 datasets and `Depends: R (>= 3.5.0)` | implemented |
+| Preserve exact legacy values, ordering, warnings, errors, and side effects | implemented; differential harness required in CI |
+| Speed up legacy `search_radius()` without changing its WGS84 results | implemented; exact benchmark parity required |
+| Vendor the exact FIPS table without changing legacy FIPS/CD results | implemented |
+| Add explicit `_ng` functions for corrected behavior and modern data | implemented |
+| Add checksum-verified, explicitly versioned bundle I/O | implemented; verified `2026.08` registry enabled |
+| Build modern data as an external bundle with provenance and quality sidecars | implemented locally |
+| Pin and archive raw inputs, dependencies, container, and build manifest | implemented; two clean `2026.08` rebuilds matched all artifact hashes |
+| Normal Windows, macOS, and Ubuntu release/devel/oldrel checks | passing on the registry-enabled PR head |
+| Public `data-2026.08` bundle and comprehensive assets, clean-machine smoke test | complete |
+
+The core package continues to ship the 2021-06-08 legacy data. Refreshed ZCTA,
+tract, and 119th-Congress relationships are available only through an explicit
+versioned bundle and `_ng` calls. USPS-only records without authoritative
+coordinates or district relationships remain unavailable or unmapped with a
+recorded reason; city-wide district inference and proxy centroids are forbidden.
+
+Documentation presents `_ng` plus an explicitly pinned modern bundle as the
+recommended path for new community code. The unsuffixed API is the historical
+compatibility path. "Recommended" never means an implicit latest version or an
+automatic data refresh: each analysis selects and records immutable bundle
+bytes.
+
+`download_zip_data()` retains its historical runtime behavior in 0.4.0.
+Documentation may discourage it, but warnings, no-ops, and removal are not
+compatible changes.
+
+## Release checklist
+
+1. Run the isolated 0.3.5 differential harness on every required CI platform.
+2. Run normal vignette-building `R CMD check --as-cran` on Windows, macOS, and
+ Ubuntu release/devel/oldrel.
+3. Build the data release twice in a clean pinned container and compare the
+ bundle, manifest, reproducibility archive, canonical content, and hashes.
+4. Commit the pipeline state used by the manifest so `working_tree_dirty` is
+ false and `pipeline_commit` names the release commit.
+5. Publish the simple bundle, manifest, reproducibility archive, and
+ comprehensive asset under an immutable data tag.
+6. Download every public asset from a clean machine and verify its checksum,
+ schema, version, and representative `_ng` calls.
+7. Only then add that exact version and checksum to the downloader registry.
+8. Rerun required CI after the registry commit. Merge only when all gates are
+ green and release claims match the evidence.
+
+## Later releases
+
+- Add data enrichments through versioned bundle schemas without changing the
+ legacy package datasets.
+- Refresh a source only through a reviewed manifest update and new immutable
+ data version.
+- Consider removing legacy dependencies or behavior only in a future major
+ version with an explicit research-migration policy. Do not silently retcon
+ the 0.3.5 contract.
+- Keep heavyweight geospatial dependencies out of package startup and the
+ `_ng` runtime path. They may remain installed and load lazily for a legacy
+ call when exact compatibility requires them; never substitute a different
+ scientific algorithm under a legacy name.
diff --git a/_pkgdown.yml b/_pkgdown.yml
index 65fb7e0..46798a6 100644
--- a/_pkgdown.yml
+++ b/_pkgdown.yml
@@ -5,3 +5,57 @@ authors:
template:
package: preferably
+
+reference:
+ - title: "Recommended API for new analyses"
+ desc: >
+ Select an immutable modern data bundle, then use the next-generation
+ functions. The bundle is always the first argument and is never selected
+ or downloaded implicitly.
+ contents:
+ - download_zip_data_bundle
+ - read_zip_data_bundle
+ - zip_data_version
+ - zip_data_provenance
+ - search_state_ng
+ - search_county_ng
+ - search_city_ng
+ - search_tz_ng
+ - search_fips_ng
+ - search_cd_ng
+ - search_radius_ng
+ - reverse_zipcode_ng
+ - geocode_zip_ng
+ - get_tracts_ng
+ - get_cd_ng
+ - is_zcta_ng
+ - normalize_zip_ng
+ - zip_distance_ng
+ - title: "Historical compatibility API"
+ desc: >
+ Existing names retain the exact 0.3.5 data and behavior for old scripts
+ and research reproduction.
+ contents:
+ - search_state
+ - search_county
+ - search_city
+ - search_tz
+ - search_fips
+ - search_cd
+ - search_radius
+ - reverse_zipcode
+ - geocode_zip
+ - get_tracts
+ - get_cd
+ - is_zcta
+ - normalize_zip
+ - zip_distance
+ - download_zip_data
+ - title: "Data objects"
+ contents:
+ - zip_code_db
+ - zcta_crosswalk
+ - zip_to_cd
+ - title: "Optional comprehensive data"
+ contents:
+ - download_comprehensive_data
diff --git a/bench/search_radius_bench.R b/bench/search_radius_bench.R
new file mode 100644
index 0000000..979baf0
--- /dev/null
+++ b/bench/search_radius_bench.R
@@ -0,0 +1,42 @@
+# Benchmark for the reproducibility-safe search_radius() optimization.
+# Run from the package root: Rscript bench/search_radius_bench.R
+
+suppressMessages(devtools::load_all(quiet = TRUE))
+`%>%` <- dplyr::`%>%`
+
+legacy_loop <- function(lat, lng, radius = 1) {
+ zip_data <- zip_code_db %>% dplyr::filter(lat != "NA")
+ for (i in seq_len(nrow(zip_data))) {
+ zip_data$distance[i] <- raster::pointDistance(
+ c(lng, lat), c(zip_data$lng[i], zip_data$lat[i]), lonlat = TRUE
+ )
+ }
+ zip_data$distance <- zip_data$distance * 0.000621371
+ zip_data %>%
+ dplyr::filter(.data$distance <= radius) %>%
+ dplyr::select(.data$zipcode, .data$distance) %>%
+ dplyr::as_tibble() %>%
+ dplyr::arrange(.data$distance)
+}
+
+cases <- list(
+ suburban_10mi = list(lat = 39.9, lng = -74.3, radius = 10),
+ urban_25mi = list(lat = 40.71, lng = -74.01, radius = 25),
+ rural_50mi = list(lat = 44.5, lng = -110.0, radius = 50)
+)
+
+for (name in names(cases)) {
+ case <- cases[[name]]
+ legacy <- legacy_loop(case$lat, case$lng, case$radius)
+ optimized <- search_radius(case$lat, case$lng, case$radius)
+ stopifnot(identical(legacy, optimized))
+
+ result <- bench::mark(
+ legacy_loop = legacy_loop(case$lat, case$lng, case$radius),
+ vectorized_wgs84 = search_radius(case$lat, case$lng, case$radius),
+ check = TRUE,
+ min_iterations = 3
+ )
+ cat("\n==", name, sprintf("(%d ZIPs in radius)\n", nrow(optimized)))
+ print(result[, c("expression", "median", "itr/sec", "mem_alloc")])
+}
diff --git a/data-raw/.gitignore b/data-raw/.gitignore
new file mode 100644
index 0000000..192e3bb
--- /dev/null
+++ b/data-raw/.gitignore
@@ -0,0 +1,3 @@
+cache/
+release/
+refresh-candidate/
diff --git a/data-raw/01_acquire.R b/data-raw/01_acquire.R
new file mode 100644
index 0000000..60f2b1f
--- /dev/null
+++ b/data-raw/01_acquire.R
@@ -0,0 +1,138 @@
+# Step 1: acquire all pipeline sources into data-raw/cache/, verifying
+# checksums for every static file. Idempotent: verified files are not
+# re-downloaded.
+
+source(file.path("data-raw", "sources.R"))
+
+cache_dir <- file.path("data-raw", "cache")
+dir.create(cache_dir, showWarnings = FALSE, recursive = TRUE)
+
+# SHA256 without adding a package dependency, mirroring file_sha256() in
+# R/data_version.R: tools::sha256sum (R >= 4.5), then the openssl package,
+# then a system binary. Minimal Linux images (r-base, Debian/Alpine slim)
+# ship sha256sum but not the shasum Perl script, so neither name can be
+# hardcoded.
+sha256_file <- function(path) {
+ if (exists("sha256sum", envir = asNamespace("tools"), inherits = FALSE)) {
+ return(unname(tools::sha256sum(path)))
+ }
+ if (requireNamespace("openssl", quietly = TRUE)) {
+ con <- file(path, "rb")
+ on.exit(close(con), add = TRUE)
+ # unclass(): as.character() on an openssl hash keeps its c("hash",
+ # "sha256") class, and a classed string is never identical() to the plain
+ # pinned hash it is compared against
+ return(unclass(as.character(openssl::sha256(con))))
+ }
+ bin <- Sys.which(c("shasum", "sha256sum"))
+ bin <- bin[nzchar(bin)][1]
+ if (is.na(bin)) {
+ stop(
+ "No SHA256 tool available to verify pipeline sources: need R >= 4.5, ",
+ "the openssl package, or a shasum/sha256sum binary on the PATH."
+ )
+ }
+ # shasum defaults to SHA-1 and needs the algorithm flag; sha256sum does not
+ args <- if (grepl("^shasum", basename(bin), ignore.case = TRUE)) {
+ c("-a", "256", shQuote(path))
+ } else {
+ shQuote(path)
+ }
+ out <- strsplit(system2(bin, args, stdout = TRUE), " ")[[1]][1]
+ if (!grepl("^[0-9a-f]{64}$", out)) {
+ stop("Unexpected output from ", bin, " while computing SHA256: ", out)
+ }
+ out
+}
+
+acquire <- function(src) {
+ dest <- file.path(cache_dir, basename(src$url))
+ if (file.exists(dest) && identical(sha256_file(dest), src$sha256)) {
+ message("cached & verified: ", basename(dest))
+ return(dest)
+ }
+ message("downloading: ", src$url)
+ utils::download.file(src$url, dest, mode = "wb", quiet = TRUE)
+ got <- sha256_file(dest)
+ if (!identical(got, src$sha256)) {
+ stop(
+ "Checksum mismatch for ", basename(dest), "\n expected: ", src$sha256,
+ "\n got: ", got,
+ "\nThe publisher may have updated the file in place. Inspect the new ",
+ "file, then update sources.R and the data release notes."
+ )
+ }
+ dest
+}
+
+paths <- lapply(PIPELINE_SOURCES, acquire)
+
+# Unpack archives
+utils::unzip(paths$gazetteer_zcta, exdir = cache_dir, overwrite = TRUE)
+utils::unzip(paths$geonames_us, exdir = cache_dir, overwrite = TRUE)
+
+# ACS response archive (API source; requires CENSUS_API_KEY only when the
+# pinned raw response is not already present). The raw JSON is the source of
+# record. The CSV consumed by later stages is deterministically derived from
+# it and separately checksummed.
+acs_raw <- file.path(cache_dir, sprintf("acs5_%d_zcta.json", ACS_VINTAGE))
+acs_cache <- file.path(cache_dir, sprintf("acs5_%d_zcta.csv", ACS_VINTAGE))
+if (!file.exists(acs_raw)) {
+ key <- Sys.getenv("CENSUS_API_KEY")
+ if (!nzchar(key)) {
+ stop(
+ "CENSUS_API_KEY is not set. Register a free key at ",
+ "https://api.census.gov/data/key_signup.html and set it in ~/.Renviron ",
+ "or as a repository secret for the refresh workflow."
+ )
+ }
+ message("downloading: ACS ", ACS_VINTAGE, " 5-year estimates for all ZCTAs")
+ url <- paste0(
+ ACS_ENDPOINT, "?get=", paste(ACS_VARIABLES, collapse = ","),
+ "&for=zip%20code%20tabulation%20area:*&key=", key
+ )
+ # The Census API only accepts the key as a query parameter, and
+ # download.file() echoes the full URL in its error and warning text. Scrub
+ # the key so a failed local run does not print it to the console or a .Rout.
+ redact <- function(x) gsub(key, "", x, fixed = TRUE)
+ withCallingHandlers(
+ tryCatch(
+ utils::download.file(url, acs_raw, mode = "wb", quiet = TRUE),
+ error = function(e) {
+ stop("ACS download failed: ", redact(conditionMessage(e)), call. = FALSE)
+ }
+ ),
+ warning = function(w) {
+ message("ACS download warning: ", redact(conditionMessage(w)))
+ invokeRestart("muffleWarning")
+ }
+ )
+}
+acs_raw_hash <- sha256_file(acs_raw)
+if (!identical(acs_raw_hash, ACS_RESPONSE_SHA256)) {
+ stop(
+ "Raw ACS response checksum mismatch. A source refresh must use a new ",
+ "explicit data version and update ACS_RESPONSE_SHA256 after review.\n",
+ " expected: ", ACS_RESPONSE_SHA256, "\n got: ", acs_raw_hash
+ )
+}
+
+j <- jsonlite::fromJSON(acs_raw)
+acs <- as.data.frame(j[-1, , drop = FALSE], stringsAsFactors = FALSE)
+names(acs) <- c(names(ACS_VARIABLES), "zcta")
+for (v in names(ACS_VARIABLES)) {
+ x <- suppressWarnings(as.numeric(acs[[v]]))
+ x[!is.na(x) & x < 0] <- NA # ACS sentinel values (-666666666 etc.)
+ acs[[v]] <- x
+}
+utils::write.csv(acs, acs_cache, row.names = FALSE)
+acs_hash <- sha256_file(acs_cache)
+if (!identical(acs_hash, ACS_DERIVED_SHA256)) {
+ stop(
+ "Derived ACS CSV checksum mismatch. The transformation or R serialization ",
+ "environment changed.\n expected: ", ACS_DERIVED_SHA256,
+ "\n got: ", acs_hash
+ )
+}
+
+message("acquire: done (", length(paths), " static sources + ACS)")
diff --git a/data-raw/02_build_zip_code_db.R b/data-raw/02_build_zip_code_db.R
new file mode 100644
index 0000000..1d254fa
--- /dev/null
+++ b/data-raw/02_build_zip_code_db.R
@@ -0,0 +1,270 @@
+# Step 2: build the refreshed zip_code_db.
+#
+# Strategy: "carry-forward + refresh". The last committed zip_code_db is the
+# base, guaranteeing no ZIP is ever silently dropped. For the 0.4.0 build that
+# base is the database AUDIT.md proves byte-identical to the uszipcode 0.2.6
+# snapshot; every later refresh carries forward from the previous data
+# release, so a value in a carry-forward-only column can only be corrected by
+# fixing it in a release rather than by rerunning the pipeline. On top of it:
+# 1. Rows new in upstream 1.0.1 are appended only when independently
+# corroborated by the pinned Census ZCTA Gazetteer. Uncorroborated
+# USPS-only and placeholder rows are quarantined, not published.
+# 2. 2020 Census ZCTAs absent from both snapshots are appended, built from
+# the Gazetteer + county relationship file + GeoNames place names.
+# 3. Curated supplemental USPS-only ZIPs are retained as review evidence but
+# are not published without an authoritative source.
+# 4. For every row whose ZIP is a 2020 ZCTA: coordinates and land/water area
+# are refreshed from the Census Gazetteer, and the five ACS attributes
+# are refreshed from the pinned ACS 5-year vintage.
+# Columns with no current public source (bounds, radius, area codes, city
+# alias lists) carry forward unchanged and are NA for new rows.
+
+suppressMessages({
+ library(dplyr)
+})
+source(file.path("data-raw", "sources.R"))
+cache_dir <- file.path("data-raw", "cache")
+
+# --- load inputs -----------------------------------------------------------
+# Read the base from git, never the working tree: 06_finalize.R overwrites
+# data/zip_code_db.rda at the end of every run, so a working-tree base would
+# make a rerun carry forward its own output instead of the last release.
+# PIPELINE_BASELINE_REF is the same ref the validation gate uses.
+base_ref <- Sys.getenv("PIPELINE_BASELINE_REF", "HEAD")
+base_tf <- tempfile(fileext = ".rda")
+base_status <- suppressWarnings(system2(
+ "git", c("show", shQuote(paste0(base_ref, ":data/zip_code_db.rda"))),
+ stdout = base_tf, stderr = FALSE
+))
+if (!identical(base_status, 0L)) {
+ stop(
+ "Cannot read the carry-forward base data/zip_code_db.rda from git ref '",
+ base_ref, "'. The pipeline builds on the last committed database."
+ )
+}
+message("carry-forward base: git ref '", base_ref, "'")
+base_env <- new.env()
+load(base_tf, envir = base_env)
+unlink(base_tf)
+base <- base_env$zip_code_db
+stopifnot(ncol(base) == 24)
+
+conn <- DBI::dbConnect(RSQLite::SQLite(), file.path(cache_dir, "simple_db.sqlite"))
+upstream101 <- DBI::dbGetQuery(conn, "SELECT * FROM simple_zipcode")
+DBI::dbDisconnect(conn)
+
+gaz <- utils::read.delim(
+ file.path(cache_dir, "2024_Gaz_zcta_national.txt"),
+ colClasses = c(GEOID = "character"), strip.white = TRUE
+)
+names(gaz) <- trimws(names(gaz))
+
+county_rel <- utils::read.delim(
+ file.path(cache_dir, "tab20_zcta520_county20_natl.txt"),
+ sep = "|", fileEncoding = "UTF-8-BOM",
+ colClasses = c(GEOID_ZCTA5_20 = "character", GEOID_COUNTY_20 = "character")
+)
+
+geonames <- utils::read.delim(
+ file.path(cache_dir, "US.txt"), header = FALSE, quote = "",
+ colClasses = "character"
+)
+names(geonames) <- c(
+ "country", "zipcode", "place", "state_name", "state", "county_name",
+ "county_code", "admin3", "admin3_code", "lat", "lng", "accuracy"
+)
+
+acs <- utils::read.csv(
+ file.path(cache_dir, sprintf("acs5_%d_zcta.csv", ACS_VINTAGE)),
+ colClasses = c(zcta = "character")
+)
+
+supplement <- utils::read.csv(
+ file.path("data-raw", "supplemental_zips.csv"),
+ colClasses = "character"
+)
+
+fips_env <- new.env()
+load(file.path("R", "sysdata.rda"), envir = fips_env)
+fips_codes <- fips_env$fips_codes
+
+# --- helper: an empty row in the exact schema of `base` --------------------
+empty_rows <- function(n) {
+ out <- base[rep(NA_integer_, n), , drop = FALSE]
+ rownames(out) <- NULL
+ out
+}
+
+titleize_type <- c(
+ "STANDARD" = "Standard", "PO BOX" = "PO Box",
+ "UNIQUE" = "Unique", "MILITARY" = "Military"
+)
+
+# --- 1. rows new in upstream 1.0.1 ----------------------------------------
+new101_all <- upstream101[!upstream101$zipcode %in% base$zipcode, , drop = FALSE]
+new101 <- new101_all[new101_all$zipcode %in% gaz$GEOID, , drop = FALSE]
+quarantined101 <- new101_all[!new101_all$zipcode %in% gaz$GEOID, , drop = FALSE]
+new101$zipcode_type <- unname(titleize_type[new101$zipcode_type])
+# 1.0.1 uses 0.0 for unknown coordinates; normalize to NA
+new101$lat[new101$lat == 0 & new101$lng == 0] <- NA_real_
+new101$lng[is.na(new101$lat)] <- NA_real_
+new101 <- new101[, names(base)]
+message(
+ "rows new in upstream 1.0.1 corroborated as Census ZCTAs: ", nrow(new101),
+ "; quarantined non-ZCTA rows: ", nrow(quarantined101)
+)
+
+# --- 2. 2020 ZCTAs missing from both snapshots ----------------------------
+known <- c(base$zipcode, new101$zipcode)
+missing_zcta <- setdiff(gaz$GEOID, known)
+message("2020 ZCTAs absent from both snapshots: ", length(missing_zcta))
+
+# predominant county per ZCTA by land-area overlap
+county_rel <- county_rel[county_rel$GEOID_ZCTA5_20 != "", ]
+predominant <- county_rel %>%
+ group_by(zcta = GEOID_ZCTA5_20) %>%
+ slice_max(AREALAND_PART, n = 1, with_ties = FALSE) %>%
+ ungroup() %>%
+ select(zcta, county_geoid = GEOID_COUNTY_20, county_name = NAMELSAD_COUNTY_20)
+
+state_by_fips <- fips_codes %>% distinct(state_code, state)
+tz_by_state <- base %>%
+ filter(!is.na(timezone)) %>%
+ count(state, timezone) %>%
+ group_by(state) %>%
+ slice_max(n, n = 1, with_ties = FALSE) %>%
+ ungroup() %>%
+ select(state, timezone)
+
+nz <- empty_rows(length(missing_zcta))
+if (length(missing_zcta) > 0) {
+ nz$zipcode <- missing_zcta
+ nz$zipcode_type <- "Standard" # by construction these are 2020 ZCTAs
+ gi <- match(missing_zcta, gaz$GEOID)
+ nz$lat <- gaz$INTPTLAT[gi]
+ nz$lng <- gaz$INTPTLONG[gi]
+ nz$land_area_in_sqmi <- gaz$ALAND_SQMI[gi]
+ nz$water_area_in_sqmi <- gaz$AWATER_SQMI[gi]
+ pi_ <- match(missing_zcta, predominant$zcta)
+ nz$county <- predominant$county_name[pi_]
+ nz$state <- state_by_fips$state[
+ match(substr(predominant$county_geoid[pi_], 1, 2), state_by_fips$state_code)
+ ]
+ gn <- geonames[!duplicated(geonames$zipcode), ]
+ gni <- match(missing_zcta, gn$zipcode)
+ nz$major_city <- gn$place[gni]
+ nz$post_office_city <- ifelse(
+ is.na(nz$major_city) | is.na(nz$state), NA_character_,
+ paste0(nz$major_city, ", ", nz$state)
+ )
+ nz$timezone <- tz_by_state$timezone[match(nz$state, tz_by_state$state)]
+}
+
+# --- 3. curated supplemental USPS-only ZIPs -------------------------------
+# These rows are review leads, not authoritative data. In particular, a point
+# chosen from a ZIP's city must not be published as that ZIP's centroid.
+supplement <- supplement[!supplement$zipcode %in% c(known, nz$zipcode), , drop = FALSE]
+message("supplemental USPS-only ZIPs quarantined: ", nrow(supplement))
+
+# Timezones for brand-new rows are IMPUTED as the modal timezone of the
+# state (no free authoritative per-ZIP source yet; the ROADMAP's
+# point-in-polygon stage replaces this). Wrong for new ZIPs in the minority
+# zone of split-timezone states - the count is tracked in the refresh summary
+# so reviewers can judge the exposure on every refresh.
+imputed_tz <- c(
+ nz$zipcode[!is.na(nz$timezone)]
+)
+
+# --- combine ---------------------------------------------------------------
+additions <- bind_rows(new101, nz)
+additions <- additions[order(additions$zipcode), ]
+zip_code_db_new <- bind_rows(base, additions)
+
+# --- 4. refresh ZCTA-backed attributes ------------------------------------
+zi <- match(zip_code_db_new$zipcode, gaz$GEOID)
+is_zcta_row <- !is.na(zi)
+zip_code_db_new$lat[is_zcta_row] <- gaz$INTPTLAT[zi[is_zcta_row]]
+zip_code_db_new$lng[is_zcta_row] <- gaz$INTPTLONG[zi[is_zcta_row]]
+zip_code_db_new$land_area_in_sqmi[is_zcta_row] <- gaz$ALAND_SQMI[zi[is_zcta_row]]
+zip_code_db_new$water_area_in_sqmi[is_zcta_row] <- gaz$AWATER_SQMI[zi[is_zcta_row]]
+
+# A coordinate from the legacy third-party database is not an authoritative
+# centroid for a USPS-only ZIP. Modern bundles therefore publish coordinates
+# only for Census-backed ZCTAs; every unavailable record receives a quality
+# reason in 06_finalize.R.
+non_zcta_row <- !is_zcta_row
+zip_code_db_new$lat[non_zcta_row] <- NA_real_
+zip_code_db_new$lng[non_zcta_row] <- NA_real_
+
+ai <- match(zip_code_db_new$zipcode, acs$zcta)
+ar <- !is.na(ai)
+as_int <- function(x) as.integer(round(x))
+zip_code_db_new$population[ar] <- as_int(acs$population[ai[ar]])
+zip_code_db_new$housing_units[ar] <- as_int(acs$housing_units[ai[ar]])
+zip_code_db_new$occupied_housing_units[ar] <- as_int(acs$occupied_housing_units[ai[ar]])
+zip_code_db_new$median_home_value[ar] <- as_int(acs$median_home_value[ai[ar]])
+zip_code_db_new$median_household_income[ar] <- as_int(acs$median_household_income[ai[ar]])
+zip_code_db_new$population_density <- ifelse(
+ !is.na(zip_code_db_new$population) &
+ !is.na(zip_code_db_new$land_area_in_sqmi) &
+ zip_code_db_new$land_area_in_sqmi > 0,
+ round(zip_code_db_new$population / zip_code_db_new$land_area_in_sqmi, 2),
+ zip_code_db_new$population_density
+)
+
+# --- clean upstream garbage coordinates -----------------------------------
+# Coordinates must come in pairs: a row with only one of lat/lng is not
+# usable, so NA both rather than shipping a half-coordinate.
+half_coord <- xor(is.na(zip_code_db_new$lat), is.na(zip_code_db_new$lng))
+if (any(half_coord)) {
+ message(
+ "clearing half-specified coordinates for ", sum(half_coord), " row(s): ",
+ paste(zip_code_db_new$zipcode[half_coord], collapse = ", ")
+ )
+ zip_code_db_new$lat[half_coord] <- NA_real_
+ zip_code_db_new$lng[half_coord] <- NA_real_
+}
+
+# A handful of upstream military rows carry junk coordinates (e.g. 09323 at
+# lat -44). NA them out rather than shipping impossible positions.
+bad_coord <- !is.na(zip_code_db_new$lat) & !is.na(zip_code_db_new$lng) &
+ (zip_code_db_new$lat < -15 | zip_code_db_new$lat > 72 |
+ zip_code_db_new$lng < -180 | zip_code_db_new$lng > 180)
+if (any(bad_coord)) {
+ message(
+ "clearing implausible coordinates for ",
+ sum(bad_coord), " row(s): ",
+ paste(zip_code_db_new$zipcode[bad_coord], collapse = ", ")
+ )
+ zip_code_db_new$lat[bad_coord] <- NA_real_
+ zip_code_db_new$lng[bad_coord] <- NA_real_
+}
+
+# --- enforce the schema contract ------------------------------------------
+zip_code_db_new <- zip_code_db_new[, names(base)]
+for (col in names(base)) {
+ if (is.integer(base[[col]])) zip_code_db_new[[col]] <- as.integer(zip_code_db_new[[col]])
+ if (is.character(base[[col]])) zip_code_db_new[[col]] <- as.character(zip_code_db_new[[col]])
+}
+zip_code_db_new <- as.data.frame(zip_code_db_new)
+rownames(zip_code_db_new) <- NULL
+
+saveRDS(zip_code_db_new, file.path(cache_dir, "zip_code_db_candidate.rds"))
+saveRDS(
+ list(
+ imputed_timezone_zips = imputed_tz,
+ quarantined_upstream_zips = quarantined101$zipcode,
+ quarantined_supplemental_zips = supplement$zipcode,
+ coordinate_unavailable_zips = zip_code_db_new$zipcode[non_zcta_row]
+ ),
+ file.path(cache_dir, "zip_code_db_stats.rds")
+)
+saveRDS(
+ list(upstream_1_0_1 = quarantined101, supplemental = supplement),
+ file.path(cache_dir, "quarantined_zip_candidates.rds")
+)
+message(
+ "zip_code_db candidate: ", nrow(zip_code_db_new), " rows (was ",
+ nrow(base), "; +", nrow(additions), "); state-modal timezone imputed for ",
+ length(imputed_tz), " new ZIP(s)"
+)
diff --git a/data-raw/03_build_zcta_crosswalk.R b/data-raw/03_build_zcta_crosswalk.R
new file mode 100644
index 0000000..677a068
--- /dev/null
+++ b/data-raw/03_build_zcta_crosswalk.R
@@ -0,0 +1,27 @@
+# Step 3: build the refreshed zcta_crosswalk (2020 ZCTA <-> 2020 tract) in
+# the modern bundle schema: tibble(ZCTA5 chr, TRACT chr(6), GEOID chr(11)).
+# GEOIDs are identifiers, not quantities; character storage prevents leading
+# zero loss and floating-point formatting in research exports.
+
+suppressMessages(library(dplyr))
+cache_dir <- file.path("data-raw", "cache")
+
+rel <- utils::read.delim(
+ file.path(cache_dir, "tab20_zcta520_tract20_natl.txt"),
+ sep = "|", fileEncoding = "UTF-8-BOM",
+ colClasses = c(GEOID_ZCTA5_20 = "character", GEOID_TRACT_20 = "character")
+)
+
+zcta_crosswalk_new <- rel %>%
+ filter(.data$GEOID_ZCTA5_20 != "") %>%
+ transmute(
+ ZCTA5 = .data$GEOID_ZCTA5_20,
+ TRACT = substr(.data$GEOID_TRACT_20, 6, 11),
+ GEOID = .data$GEOID_TRACT_20
+ ) %>%
+ distinct() %>%
+ arrange(.data$ZCTA5, .data$GEOID) %>%
+ as_tibble()
+
+saveRDS(zcta_crosswalk_new, file.path(cache_dir, "zcta_crosswalk_candidate.rds"))
+message("zcta_crosswalk candidate: ", nrow(zcta_crosswalk_new), " rows (2020 vintage)")
diff --git a/data-raw/04_build_zip_to_cd.R b/data-raw/04_build_zip_to_cd.R
new file mode 100644
index 0000000..731451b
--- /dev/null
+++ b/data-raw/04_build_zip_to_cd.R
@@ -0,0 +1,50 @@
+# Step 4: build the refreshed zip_to_cd (ZIP <-> 119th-Congress district) in
+# the exact schema of the shipped dataset: data.frame(ZIP chr, CD chr(4) =
+# state FIPS + 2-digit district), multiple rows for multi-district ZIPs.
+#
+# Method follows PR #30 by @awallender (which used the CD118 vintage),
+# updated to the 119th-Congress relationship file, with two additions:
+#
+# - Census "ZZ" pseudo-district rows (the not-in-any-district offshore/water
+# remainder) are excluded - they are not congressional districts.
+# - The relationship file covers 2020 ZCTAs, not all USPS ZIP codes. USPS-only
+# codes are deliberately left unmapped: city-wide and state-wide inference
+# can over-assign districts and is not defensible for research use.
+
+suppressMessages(library(dplyr))
+cache_dir <- file.path("data-raw", "cache")
+
+rel <- utils::read.delim(
+ file.path(cache_dir, "tab20_cd11920_zcta520_natl.txt"),
+ sep = "|", fileEncoding = "UTF-8-BOM",
+ colClasses = c(GEOID_CD119_20 = "character", GEOID_ZCTA5_20 = "character")
+)
+
+zcta_cd <- rel %>%
+ filter(
+ .data$GEOID_ZCTA5_20 != "",
+ grepl("^[0-9]{4}$", .data$GEOID_CD119_20) # drops "" and ZZ pseudo-districts
+ ) %>%
+ transmute(ZIP = .data$GEOID_ZCTA5_20, CD = .data$GEOID_CD119_20) %>%
+ distinct()
+
+# Load the candidate only to document which ZIPs remain unmapped.
+zipdb <- readRDS(file.path(cache_dir, "zip_code_db_candidate.rds"))
+unmapped <- setdiff(zipdb$zipcode, zcta_cd$ZIP)
+counts <- list(
+ zcta_mapped = length(unique(zcta_cd$ZIP)),
+ city_derived = 0L,
+ state_derived = 0L,
+ unmapped = length(unmapped)
+)
+message(
+ "zip_to_cd: ", counts$zcta_mapped, " authoritative ZCTA-mapped ZIPs; ",
+ counts$unmapped, " ZIPs intentionally unmapped (not represented by a ZCTA)"
+)
+zip_to_cd_new <- zcta_cd %>%
+ arrange(.data$ZIP, .data$CD) %>%
+ as.data.frame()
+
+saveRDS(zip_to_cd_new, file.path(cache_dir, "zip_to_cd_candidate.rds"))
+saveRDS(counts, file.path(cache_dir, "zip_to_cd_stats.rds"))
+message("zip_to_cd candidate: ", nrow(zip_to_cd_new), " rows (CD119 vintage)")
diff --git a/data-raw/05_validate.R b/data-raw/05_validate.R
new file mode 100644
index 0000000..f5b2380
--- /dev/null
+++ b/data-raw/05_validate.R
@@ -0,0 +1,255 @@
+# Step 5: validation gate. Every check here must pass before refreshed data
+# ships. Aborts with a full failure list otherwise, and writes
+# data-raw/refresh_summary.md describing the diff for the release PR.
+
+suppressMessages(library(dplyr))
+source(file.path("data-raw", "sources.R"))
+cache_dir <- file.path("data-raw", "cache")
+
+candidate <- readRDS(file.path(cache_dir, "zip_code_db_candidate.rds"))
+zcta_candidate <- readRDS(file.path(cache_dir, "zcta_crosswalk_candidate.rds"))
+cd_candidate <- readRDS(file.path(cache_dir, "zip_to_cd_candidate.rds"))
+
+# Baseline = the LAST COMMITTED data files, taken from git - never the
+# working tree, which 06_finalize.R overwrites (a working-tree baseline
+# would compare a rerun candidate against itself and pass every regression
+# check trivially). Override the ref with PIPELINE_BASELINE_REF to compare
+# against another release (e.g. master).
+baseline_ref <- Sys.getenv("PIPELINE_BASELINE_REF", "HEAD")
+load_baseline <- function(name) {
+ tf <- tempfile(fileext = ".rda")
+ status <- suppressWarnings(system2(
+ "git", c("show", shQuote(paste0(baseline_ref, ":data/", name))),
+ stdout = tf, stderr = FALSE
+ ))
+ if (!identical(status, 0L)) {
+ stop(
+ "Cannot read baseline data/", name, " from git ref '", baseline_ref,
+ "'. The validation gate needs the last committed data as its baseline."
+ )
+ }
+ e <- new.env()
+ load(tf, envir = e)
+ unlink(tf)
+ e[[ls(e)[1]]]
+}
+message("validation baseline: git ref '", baseline_ref, "'")
+shipped_env <- new.env()
+shipped_env$zip_code_db <- load_baseline("zip_code_db.rda")
+shipped_env$zcta_crosswalk <- load_baseline("zcta_crosswalk.rda")
+shipped_env$zip_to_cd <- load_baseline("zip_to_cd.rda")
+shipped <- shipped_env$zip_code_db
+
+failures <- character()
+check <- function(ok, label) {
+ status <- if (isTRUE(ok)) "PASS" else "FAIL"
+ message(sprintf("[%s] %s", status, label))
+ if (!isTRUE(ok)) failures <<- c(failures, label)
+ invisible(ok)
+}
+
+# --- zip_code_db -----------------------------------------------------------
+# Relative, not an absolute constant: the row count is monotonically
+# non-decreasing by construction (>= shipped, plus the no-silent-drops check
+# below), so a fixed ceiling would eventually fail a legitimate refresh with
+# what reads like a data-corruption alarm.
+row_ceiling <- ceiling(nrow(shipped) * 1.1)
+check(
+ nrow(candidate) >= nrow(shipped) && nrow(candidate) <= row_ceiling,
+ sprintf("row count in sane bounds (%d, was %d, ceiling %d)",
+ nrow(candidate), nrow(shipped), row_ceiling)
+)
+dropped <- setdiff(shipped$zipcode, candidate$zipcode)
+check(length(dropped) == 0, sprintf("no silent drops (%d dropped)", length(dropped)))
+check(!anyDuplicated(candidate$zipcode), "zipcode is unique")
+check(identical(names(candidate), names(shipped)), "column names identical")
+check(
+ identical(unname(sapply(candidate, function(x) class(x)[1])),
+ unname(sapply(shipped, function(x) class(x)[1]))),
+ "column classes identical"
+)
+check(identical(class(candidate), class(shipped)), "object class identical (data.frame)")
+
+allowed_types <- c("Standard", "PO Box", "Unique", "Military", NA)
+check(
+ all(candidate$zipcode_type %in% allowed_types),
+ "zipcode_type values within the documented set"
+)
+
+# Regression ZCTAs from #25 / #26 / #19 must be present with authoritative
+# coordinates. USPS-only candidates such as 91230 are tested as quarantined
+# below rather than being assigned a city proxy point.
+regression_zips <- c(
+ "97003",
+ "00802", "00820", "00830", "00840", "00850", "00851", # USVI
+ "96799", # American Samoa
+ "96910", "96913", "96915", "96916", "96917", "96928", "96929", # Guam
+ "96950", "96951", "96952", # N. Mariana
+ "72405", "72713", "75036", "75072", "89437" # new mainland ZCTAs
+)
+ri <- match(regression_zips, candidate$zipcode)
+check(!anyNA(ri), "all regression ZIPs (#19/#25/#26) present")
+check(
+ !anyNA(candidate$lat[ri]) && !anyNA(candidate$lng[ri]),
+ "all regression ZIPs have coordinates"
+)
+quarantine <- readRDS(file.path(cache_dir, "quarantined_zip_candidates.rds"))
+quarantined_zips <- unique(c(
+ quarantine$upstream_1_0_1$zipcode,
+ quarantine$supplemental$zipcode
+))
+check(
+ all(c("91230", "88888", "72643") %in% quarantined_zips),
+ "uncorroborated/proxy ZIP candidates 91230, 88888, and 72643 are quarantined"
+)
+check(
+ !any(c("91230", "88888", "72643") %in% candidate$zipcode),
+ "quarantined ZIP candidates are absent from the published candidate"
+)
+
+# known-good distance spot checks (the #20 examples), via the package's own
+# haversine; tolerance covers coordinate-precision refreshes
+hav <- function(lat1, lng1, lat2, lng2) {
+ tr <- pi / 180
+ h <- sin((lat2 - lat1) * tr / 2)^2 +
+ cos(lat1 * tr) * cos(lat2 * tr) * sin((lng2 - lng1) * tr / 2)^2
+ 2 * 6371008.8 * asin(pmin(1, sqrt(h))) * 0.000621371
+}
+dist_zip <- function(a, b) {
+ ia <- match(a, candidate$zipcode); ib <- match(b, candidate$zipcode)
+ hav(candidate$lat[ia], candidate$lng[ia], candidate$lat[ib], candidate$lng[ib])
+}
+# Ranges catch pair swaps and coordinate corruption while tolerating
+# legitimate centroid-precision changes between data vintages. The #20
+# reprex distinguishes a ~44mi leg from a ~10mi leg.
+d1 <- dist_zip("08731", "08901")
+d2 <- dist_zip("08734", "08005")
+check(d1 > 35 && d1 < 50, sprintf("distance spot check 08731->08901 in [35,50] (%.1f)", d1))
+check(d2 > 5 && d2 < 15, sprintf("distance spot check 08734->08005 in [5,15] (%.1f)", d2))
+check(d1 > 2 * d2, "distance spot check: long leg dominates short leg (no pair swap)")
+check(dist_zip("08731", "08731") == 0, "distance spot check identity = 0")
+
+# coordinate sanity: US bounding envelope (incl. territories/military NA-safe)
+with_coords <- !is.na(candidate$lat)
+check(
+ all(candidate$lat[with_coords] >= -15 & candidate$lat[with_coords] <= 72) &&
+ all(candidate$lng[with_coords] >= -180 & candidate$lng[with_coords] <= 180),
+ "coordinates within plausible envelope"
+)
+check(
+ sum(!with_coords) <= sum(is.na(shipped$lat)) + 1000,
+ "share of coordinate-less ZIPs did not grow materially"
+)
+
+# --- zcta_crosswalk --------------------------------------------------------
+check(
+ nrow(zcta_candidate) > 100000,
+ sprintf("zcta_crosswalk row count sane (%d)", nrow(zcta_candidate))
+)
+check(
+ identical(names(zcta_candidate), names(shipped_env$zcta_crosswalk)),
+ "zcta_crosswalk schema identical"
+)
+check(
+ all(nchar(zcta_candidate$TRACT) == 6),
+ "zcta_crosswalk TRACT codes are 6 characters"
+)
+check(
+ is.character(zcta_candidate$GEOID) &&
+ all(grepl("^[0-9]{11}$", zcta_candidate$GEOID)),
+ "zcta_crosswalk GEOID values are 11-character identifiers"
+)
+
+# --- zip_to_cd -------------------------------------------------------------
+check(
+ identical(names(cd_candidate), names(shipped_env$zip_to_cd)),
+ "zip_to_cd schema identical"
+)
+check(
+ all(grepl("^[0-9]{4}$", cd_candidate$CD)),
+ "zip_to_cd CD codes are 4 digits (no ZZ pseudo-districts)"
+)
+check(
+ length(setdiff(zcta_candidate$ZCTA5, cd_candidate$ZIP)) < 500,
+ "zip_to_cd covers (nearly) all 2020 ZCTAs"
+)
+# The next-generation crosswalk is authoritative-only. Coverage differences
+# versus the pre-2020 HUD-USPS product are reported, not papered over with
+# city/state inference.
+prev_covered <- intersect(shipped_env$zip_to_cd$ZIP, candidate$zipcode)
+lost_cd <- setdiff(prev_covered, cd_candidate$ZIP)
+message(
+ "[NOTE] ", length(lost_cd),
+ " legacy ZIP-to-CD mappings are absent from the authoritative ZCTA crosswalk"
+)
+
+# --- release metadata ------------------------------------------------------
+check(
+ is.list(COMPREHENSIVE_RELEASE) &&
+ grepl("^data-", COMPREHENSIVE_RELEASE$release_tag) &&
+ grepl("^[0-9a-f]{64}$", COMPREHENSIVE_RELEASE$sha256),
+ "COMPREHENSIVE_RELEASE registry is well-formed"
+)
+if (!identical(COMPREHENSIVE_RELEASE$release_tag, paste0("data-", DATA_VERSION))) {
+ message(
+ "[NOTE] comprehensive asset pinned to ", COMPREHENSIVE_RELEASE$release_tag,
+ " (data release is data-", DATA_VERSION,
+ ") - expected unless a new comprehensive asset was published"
+ )
+}
+
+# --- summary for the release PR -------------------------------------------
+added <- setdiff(candidate$zipcode, shipped$zipcode)
+common <- intersect(candidate$zipcode, shipped$zipcode)
+ci <- match(common, candidate$zipcode); si <- match(common, shipped$zipcode)
+coord_changed <- sum(
+ !is.na(candidate$lat[ci]) & !is.na(shipped$lat[si]) &
+ (abs(candidate$lat[ci] - shipped$lat[si]) > 1e-6 |
+ abs(candidate$lng[ci] - shipped$lng[si]) > 1e-6)
+)
+pop_changed <- sum(
+ is.na(candidate$population[ci]) != is.na(shipped$population[si]) |
+ coalesce(candidate$population[ci] != shipped$population[si], FALSE)
+)
+
+summary_md <- c(
+ "## Data refresh summary",
+ "",
+ sprintf("- `zip_code_db`: %d rows (was %d): **%d added, 0 removed**",
+ nrow(candidate), nrow(shipped), length(added)),
+ sprintf(" - added by type: %s",
+ paste(sprintf("%s (%d)", names(table(candidate$zipcode_type[match(added, candidate$zipcode)], useNA = "ifany")),
+ table(candidate$zipcode_type[match(added, candidate$zipcode)], useNA = "ifany")), collapse = ", ")),
+ sprintf(" - coordinates refreshed for %d existing ZIPs; ACS attributes refreshed for %d ZIPs",
+ coord_changed, pop_changed),
+ sprintf("- `zcta_crosswalk`: %d rows, 2020 ZCTA/tract vintage (previously %d rows)",
+ nrow(zcta_candidate), nrow(shipped_env$zcta_crosswalk)),
+ sprintf("- `zip_to_cd`: %d rows, 119th-Congress vintage (previously %d rows)",
+ nrow(cd_candidate), nrow(shipped_env$zip_to_cd)),
+ local({
+ s <- readRDS(file.path(cache_dir, "zip_to_cd_stats.rds"))
+ sprintf(
+ " - %d authoritative ZCTA-mapped ZIPs; %d ZIPs intentionally unmapped; no city/state-derived assignments",
+ s$zcta_mapped, s$unmapped
+ )
+ }),
+ sprintf(" - %d pre-2020 legacy mappings not carried into the authoritative-only crosswalk", length(lost_cd)),
+ local({
+ s <- readRDS(file.path(cache_dir, "zip_code_db_stats.rds"))
+ sprintf(
+ "- state-modal timezone imputed for %d new ZIP(s)%s",
+ length(s$imputed_timezone_zips),
+ if (length(s$imputed_timezone_zips) > 0 && length(s$imputed_timezone_zips) <= 20) {
+ paste0(": ", paste(s$imputed_timezone_zips, collapse = ", "))
+ } else ""
+ )
+ }),
+ "",
+ sprintf("Candidate data validation gate: %s", if (length(failures) == 0) "**passed**" else "**FAILED**")
+)
+writeLines(summary_md, file.path("data-raw", "refresh_summary.md"))
+
+if (length(failures) > 0) {
+ stop("Validation gate FAILED:\n - ", paste(failures, collapse = "\n - "))
+}
+message("candidate data validation gate: passed")
diff --git a/data-raw/06_finalize.R b/data-raw/06_finalize.R
new file mode 100644
index 0000000..14b215c
--- /dev/null
+++ b/data-raw/06_finalize.R
@@ -0,0 +1,315 @@
+# Step 6: package validated candidates as an immutable external data bundle.
+# This stage never writes data/ or R/sysdata.rda; those files are the frozen
+# zipcodeR 0.3.5 compatibility contract.
+
+source(file.path("data-raw", "sources.R"))
+cache_dir <- file.path("data-raw", "cache")
+release_dir <- file.path("data-raw", "release")
+dir.create(release_dir, recursive = TRUE, showWarnings = FALSE)
+
+zip_code_db <- readRDS(file.path(cache_dir, "zip_code_db_candidate.rds"))
+zcta_crosswalk <- readRDS(file.path(cache_dir, "zcta_crosswalk_candidate.rds"))
+zip_to_cd <- readRDS(file.path(cache_dir, "zip_to_cd_candidate.rds"))
+stats <- readRDS(file.path(cache_dir, "zip_code_db_stats.rds"))
+
+build_timestamp <- Sys.getenv("PIPELINE_BUILD_TIMESTAMP")
+if (!nzchar(build_timestamp)) {
+ stop("PIPELINE_BUILD_TIMESTAMP must be an explicit ISO-8601 timestamp.")
+}
+
+hash_file <- function(path) {
+ if (exists("sha256_file", mode = "function")) return(sha256_file(path))
+ if (exists("sha256sum", envir = asNamespace("tools"), inherits = FALSE)) {
+ return(unname(tools::sha256sum(path)))
+ }
+ con <- file(path, "rb")
+ on.exit(close(con), add = TRUE)
+ unclass(as.character(openssl::sha256(con)))
+}
+
+hash_object <- function(object) {
+ path <- tempfile(fileext = ".rds")
+ on.exit(unlink(path), add = TRUE)
+ saveRDS(object, path, version = 3, compress = "xz")
+ hash_file(path)
+}
+
+git_output <- function(args) {
+ out <- suppressWarnings(system2("git", args, stdout = TRUE, stderr = TRUE))
+ if (!is.null(attr(out, "status"))) return(NA_character_)
+ paste(out, collapse = "\n")
+}
+
+pipeline_commit <- git_output(c("rev-parse", "HEAD"))
+working_tree_dirty <- nzchar(git_output(c("status", "--porcelain")))
+
+source_files <- c(
+ zcta_tract_rel = file.path(cache_dir, basename(PIPELINE_SOURCES$zcta_tract_rel$url)),
+ zcta_county_rel = file.path(cache_dir, basename(PIPELINE_SOURCES$zcta_county_rel$url)),
+ cd_zcta_rel = file.path(cache_dir, basename(PIPELINE_SOURCES$cd_zcta_rel$url)),
+ gazetteer_zcta = file.path(cache_dir, basename(PIPELINE_SOURCES$gazetteer_zcta$url)),
+ geonames_us = file.path(cache_dir, basename(PIPELINE_SOURCES$geonames_us$url)),
+ uszipcode_simple_101 = file.path(cache_dir, basename(PIPELINE_SOURCES$uszipcode_simple_101$url)),
+ acs_2023_raw = file.path(cache_dir, sprintf("acs5_%d_zcta.json", ACS_VINTAGE)),
+ acs_2023_derived = file.path(cache_dir, sprintf("acs5_%d_zcta.csv", ACS_VINTAGE))
+)
+if (!all(file.exists(source_files))) {
+ stop("Cannot finalize: one or more archived source files are missing.")
+}
+
+source_manifest <- lapply(names(source_files), function(id) {
+ registry <- PIPELINE_SOURCES[[id]]
+ is_acs_raw <- identical(id, "acs_2023_raw")
+ is_acs_derived <- identical(id, "acs_2023_derived")
+ list(
+ id = id,
+ file = basename(source_files[[id]]),
+ kind = if (is_acs_raw) {
+ "archived_api_response"
+ } else if (is_acs_derived) {
+ "deterministically_derived_artifact"
+ } else {
+ "archived_download"
+ },
+ url = if (is_acs_raw) ACS_ENDPOINT else if (is_acs_derived) NULL else registry$url,
+ request = if (is_acs_raw) {
+ list(
+ get = unname(ACS_VARIABLES),
+ geography = "zip code tabulation area:*",
+ authentication = "Census API key required but never archived"
+ )
+ } else {
+ NULL
+ },
+ derived_from = if (is_acs_derived) "acs_2023_raw" else NULL,
+ sha256 = hash_file(source_files[[id]]),
+ license = if (is.null(registry)) {
+ "U.S. public domain (U.S. Census Bureau)"
+ } else {
+ registry$license
+ },
+ vintage = if (grepl("^acs_2023", id)) as.character(ACS_VINTAGE) else NULL
+ )
+})
+
+unmapped_cd <- setdiff(zip_code_db$zipcode, zip_to_cd$ZIP)
+cd_quality <- data.frame(
+ dataset = "zip_to_cd",
+ key = unmapped_cd,
+ field = "CD",
+ status = "unmapped",
+ reason = paste(
+ "No authoritative 2020-ZCTA-to-CD119 relationship;",
+ "no city/state inference applied"
+ ),
+ stringsAsFactors = FALSE
+)
+zcta_keys <- unique(zcta_crosswalk$ZCTA5)
+coordinate_is_authoritative <- zip_code_db$zipcode %in% zcta_keys &
+ !is.na(zip_code_db$lat) & !is.na(zip_code_db$lng)
+coordinate_quality <- data.frame(
+ dataset = "zip_code_db",
+ key = zip_code_db$zipcode,
+ field = "lat,lng",
+ status = ifelse(coordinate_is_authoritative, "authoritative", "unavailable"),
+ reason = ifelse(
+ coordinate_is_authoritative,
+ "Census ZCTA internal point",
+ "No authoritative coordinate for this non-ZCTA or unavailable ZCTA"
+ ),
+ stringsAsFactors = FALSE
+)
+acs_keys <- utils::read.csv(
+ file.path(cache_dir, sprintf("acs5_%d_zcta.csv", ACS_VINTAGE)),
+ colClasses = c(zcta = "character")
+)$zcta
+demographic_quality <- data.frame(
+ dataset = "zip_code_db",
+ key = zip_code_db$zipcode,
+ field = "demographics",
+ status = ifelse(
+ zip_code_db$zipcode %in% acs_keys,
+ "authoritative_current_vintage",
+ "legacy_carried_forward_or_missing"
+ ),
+ reason = ifelse(
+ zip_code_db$zipcode %in% acs_keys,
+ paste0("ACS ", ACS_VINTAGE, " 5-year ZCTA estimates"),
+ "No matching ACS ZCTA; values may be legacy carry-forward or missing"
+ ),
+ stringsAsFactors = FALSE
+)
+quality <- rbind(cd_quality, coordinate_quality, demographic_quality)
+quarantine <- unique(c(
+ stats$quarantined_upstream_zips,
+ stats$quarantined_supplemental_zips
+))
+quarantine_provenance <- data.frame(
+ dataset = "zip_code_db",
+ key = quarantine,
+ field = "record",
+ source_id = "quarantine",
+ method = "excluded pending authoritative corroboration",
+ quality = "quarantined",
+ note = "Not present in the published bundle",
+ stringsAsFactors = FALSE
+)
+dataset_provenance <- data.frame(
+ dataset = c("zip_code_db", "zip_code_db", "zcta_crosswalk", "zip_to_cd"),
+ key = "*",
+ field = c("coordinates_and_area", "demographics", "record", "record"),
+ source_id = c("gazetteer_zcta", "acs_2023_derived", "zcta_tract_rel", "cd_zcta_rel"),
+ method = c(
+ "Census ZCTA internal points and area",
+ "ACS 5-year ZCTA estimates",
+ "direct Census relationship",
+ "direct Census relationship; ZZ pseudo-districts excluded"
+ ),
+ quality = "authoritative_source",
+ note = c(
+ "Applies only to Census-backed ZCTA rows",
+ "Applies only to rows matched to the archived ACS response",
+ "",
+ "USPS-only ZIPs are intentionally unmapped"
+ ),
+ stringsAsFactors = FALSE
+)
+provenance <- rbind(dataset_provenance, quarantine_provenance)
+output_hashes <- list(
+ zip_code_db = hash_object(zip_code_db),
+ zcta_crosswalk = hash_object(zcta_crosswalk),
+ zip_to_cd = hash_object(zip_to_cd),
+ provenance = hash_object(provenance),
+ quality = hash_object(quality)
+)
+
+metadata <- list(
+ data_version = DATA_VERSION,
+ build_timestamp = build_timestamp,
+ pipeline_commit = pipeline_commit,
+ working_tree_dirty = working_tree_dirty,
+ r_version = R.version.string,
+ dependency_lock_sha256 = hash_file(file.path("data-raw", "pkg.lock")),
+ pak_bootstrap_sha256 = hash_file(file.path("data-raw", "vendor", "pak_0.11.1.tar.gz")),
+ output_hashes = output_hashes,
+ rows = list(
+ zip_code_db = nrow(zip_code_db),
+ zcta_crosswalk = nrow(zcta_crosswalk),
+ zip_to_cd = nrow(zip_to_cd)
+ ),
+ sources = source_manifest,
+ policies = list(
+ legacy_defaults = "zipcodeR 0.3.5 datasets remain bundled in the package",
+ non_zcta_additions = "quarantined unless authoritatively corroborated",
+ congressional_districts = "authoritative Census ZCTA relationships only"
+ )
+)
+
+bundle <- structure(
+ list(
+ zip_code_db = zip_code_db,
+ zcta_crosswalk = zcta_crosswalk,
+ zip_to_cd = zip_to_cd,
+ metadata = metadata,
+ provenance = provenance,
+ quality = quality
+ ),
+ class = c("zipcodeR_data_bundle", "list")
+)
+
+asset <- sprintf("zipcodeR-data-%s.rds", DATA_VERSION)
+asset_path <- file.path(release_dir, asset)
+saveRDS(bundle, asset_path, version = 3, compress = "xz")
+asset_sha256 <- hash_file(asset_path)
+
+manifest <- list(
+ format = 1,
+ data_version = DATA_VERSION,
+ release_tag = paste0("data-", DATA_VERSION),
+ asset = asset,
+ asset_sha256 = asset_sha256,
+ asset_size = unname(file.info(asset_path)$size),
+ build_timestamp = build_timestamp,
+ pipeline_commit = pipeline_commit,
+ working_tree_dirty = working_tree_dirty,
+ r_version = metadata$r_version,
+ dependency_lock_sha256 = metadata$dependency_lock_sha256,
+ pak_bootstrap_sha256 = metadata$pak_bootstrap_sha256,
+ output_hashes = output_hashes,
+ schemas = list(
+ zip_code_db = lapply(names(zip_code_db), function(name) {
+ list(name = name, type = class(zip_code_db[[name]])[1])
+ }),
+ zcta_crosswalk = lapply(names(zcta_crosswalk), function(name) {
+ list(name = name, type = class(zcta_crosswalk[[name]])[1])
+ }),
+ zip_to_cd = lapply(names(zip_to_cd), function(name) {
+ list(name = name, type = class(zip_to_cd[[name]])[1])
+ })
+ ),
+ rows = metadata$rows,
+ sources = source_manifest
+)
+manifest_path <- file.path(release_dir, sprintf("manifest-%s.json", DATA_VERSION))
+jsonlite::write_json(
+ manifest, manifest_path, auto_unbox = TRUE, pretty = TRUE, null = "null"
+)
+
+repro_files <- c(
+ list.files(
+ "data-raw", pattern = "\\.(R|md|json|csv|txt)$", full.names = TRUE
+ ),
+ file.path("data-raw", "Dockerfile"),
+ file.path("data-raw", "pkg.lock"),
+ file.path("data-raw", "LICENSES.md"),
+ file.path("data-raw", "vendor", "pak_0.11.1.tar.gz"),
+ source_files,
+ manifest_path,
+ "DESCRIPTION"
+)
+repro_files <- unique(repro_files[file.exists(repro_files)])
+repro_path <- file.path(
+ release_dir, sprintf("zipcodeR-reproducibility-%s.tar.gz", DATA_VERSION)
+)
+
+# Archive from a staging tree with a fixed timestamp and mode. Several files
+# above are deterministically regenerated on every pass; archiving their live
+# filesystem mtimes would make the reproducibility archive itself change even
+# when every byte of its content is identical.
+repro_stage <- tempfile("zipcodeR-repro-")
+dir.create(repro_stage)
+for (source in repro_files) {
+ destination <- file.path(repro_stage, source)
+ dir.create(dirname(destination), recursive = TRUE, showWarnings = FALSE)
+ if (!file.copy(source, destination, overwrite = TRUE, copy.mode = FALSE,
+ copy.date = FALSE)) {
+ stop("Failed to stage reproducibility input: ", source)
+ }
+ Sys.chmod(destination, mode = "0644")
+}
+archive_time <- as.POSIXct(build_timestamp, format = "%Y-%m-%dT%H:%M:%SZ", tz = "UTC")
+if (is.na(archive_time)) stop("PIPELINE_BUILD_TIMESTAMP is not valid ISO-8601 UTC.")
+staged_files <- file.path(repro_stage, repro_files)
+invisible(lapply(staged_files, Sys.setFileTime, time = archive_time))
+repro_path_absolute <- file.path(normalizePath(release_dir), basename(repro_path))
+write_reproducibility_archive <- function() {
+ old_working_directory <- setwd(repro_stage)
+ on.exit(setwd(old_working_directory), add = TRUE)
+ utils::tar(
+ repro_path_absolute,
+ files = repro_files,
+ compression = "gzip",
+ tar = "internal"
+ )
+}
+write_reproducibility_archive()
+unlink(repro_stage, recursive = TRUE)
+
+message("bundle: ", asset_path, " (sha256 ", asset_sha256, ")")
+message("manifest: ", manifest_path)
+message("reproducibility archive: ", repro_path)
+if (working_tree_dirty) {
+ message(
+ "NOTE: working tree is dirty; artifacts are test candidates and must not be published."
+ )
+}
diff --git a/data-raw/Dockerfile b/data-raw/Dockerfile
new file mode 100644
index 0000000..1962230
--- /dev/null
+++ b/data-raw/Dockerfile
@@ -0,0 +1,26 @@
+FROM rocker/r-ver:4.6.1@sha256:a5df0ae591422cc1733e97da03a9f4eff4cf172e40895225959aa93bb7ff7517
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends libcurl4-openssl-dev libssl-dev \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY data-raw/vendor/pak_0.11.1.tar.gz /tmp/pak_0.11.1.tar.gz
+RUN echo "762ec8acc2d2ccbe5a217859c846c2de83254e3948112b437dde7a0ad5b15999 /tmp/pak_0.11.1.tar.gz" | sha256sum --check \
+ && R CMD INSTALL /tmp/pak_0.11.1.tar.gz
+
+COPY data-raw/pkg.lock /tmp/zipcodeR-pkg.lock
+RUN Rscript -e 'pak::lockfile_install("/tmp/zipcodeR-pkg.lock")'
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends git \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /work/zipcodeR
+COPY . .
+
+ENV PIPELINE_DATA_VERSION=2026.08
+ENV PIPELINE_BUILD_TIMESTAMP=2026-08-24T00:00:00Z
+ENV PIPELINE_BASELINE_REF=master
+ENV PIPELINE_MODE=rebuild
+
+CMD ["Rscript", "data-raw/run_pipeline.R"]
diff --git a/data-raw/LICENSES.md b/data-raw/LICENSES.md
new file mode 100644
index 0000000..c04ed16
--- /dev/null
+++ b/data-raw/LICENSES.md
@@ -0,0 +1,15 @@
+# Data-source license notices
+
+The reproducibility archive contains exact source bytes so a published data
+release can be rebuilt without depending on mutable upstream URLs.
+
+- U.S. Census Bureau relationship files, Gazetteer files, and ACS responses
+ are works of the United States government and are treated as U.S. public
+ domain data.
+- GeoNames postal-code data is provided under CC BY 4.0. Attribution:
+ GeoNames, .
+- `uszipcode-project` database snapshots are provided under the MIT License.
+ Copyright and attribution remain with MacHu-GWU/uszipcode-project.
+
+These notices describe the upstream materials; zipcodeR's package code
+continues to use the license declared in `DESCRIPTION`.
diff --git a/data-raw/README.md b/data-raw/README.md
new file mode 100644
index 0000000..f717299
--- /dev/null
+++ b/data-raw/README.md
@@ -0,0 +1,106 @@
+# zipcodeR versioned data assets
+
+The datasets in `data/` are the frozen zipcodeR 0.3.5 compatibility contract.
+This pipeline never replaces them. It builds an opt-in, versioned RDS bundle
+for the `_ng` API and a separate reproducibility archive for a data-only
+GitHub release.
+
+## Two deliberately separate modes
+
+`refresh_sources.R` inspects mutable upstream URLs for a proposed *new* data
+version. It downloads candidate source bytes and writes
+`proposed-sources.json`; it does not change pins, build data, or publish.
+
+```sh
+PIPELINE_MODE=refresh \
+PIPELINE_DATA_VERSION=2027.01 \
+PIPELINE_PROPOSED_VERSION=2027.01 \
+PIPELINE_BUILD_TIMESTAMP=2027-01-15T00:00:00Z \
+Rscript data-raw/refresh_sources.R
+```
+
+After human review, a maintainer updates `sources.R` with exact URLs,
+vintages, and SHA256 values. `run_pipeline.R` then performs a deterministic
+rebuild of that explicitly pinned version:
+
+```sh
+PIPELINE_MODE=rebuild \
+PIPELINE_DATA_VERSION=2026.08 \
+PIPELINE_BUILD_TIMESTAMP=2026-08-24T00:00:00Z \
+PIPELINE_BASELINE_REF=master \
+Rscript data-raw/run_pipeline.R
+```
+
+There is no `latest` alias and no release identity derived from the current
+date. A missing source is downloaded only from its pinned URL and must match
+its recorded checksum. The raw ACS JSON response and its deterministically
+derived CSV are both checksummed and archived.
+
+## Pipeline stages
+
+| File | Purpose |
+|---|---|
+| `sources.R` | Explicit data identity, source URLs, vintages, SHA256 values, and licenses. |
+| `01_acquire.R` | Acquire and verify exact static sources and the archived raw ACS response. |
+| `02_build_zip_code_db.R` | Refresh Census-backed attributes; add only independently corroborated ZCTAs; quarantine other candidates. |
+| `03_build_zcta_crosswalk.R` | Build the 2020 ZCTA-to-tract relationship with character GEOIDs. |
+| `04_build_zip_to_cd.R` | Build authoritative CD119-to-ZCTA relationships only. |
+| `05_validate.R` | Reject schema, identifier, coordinate, quarantine, or mapping-policy violations. |
+| `06_finalize.R` | Write the external bundle, manifest, hashes, quality tables, and reproducibility archive. |
+
+The pinned R toolchain is described by `Dockerfile`; all pipeline package
+dependencies, including transitive dependencies, are locked with source
+archive hashes in `pkg.lock`. The exact `pak` bootstrap source is vendored in
+`vendor/` and checksum-verified before installation.
+
+## ZIP and ZCTA policy
+
+ZIP codes are USPS delivery constructs; ZCTAs are Census statistical areas.
+The pipeline does not present a city point as a ZIP centroid and does not
+assume that an entry in a third-party ZIP database is a Census ZCTA.
+
+- Rows independently present in the pinned Census ZCTA Gazetteer may enter the
+ modern `zip_code_db`.
+- Uncorroborated upstream and supplemental candidates—including proxy or
+ placeholder records such as 91230, 88888, and 72643—are quarantined and
+ exposed in provenance artifacts, not published as authoritative rows.
+- Coordinates come from Census ZCTA internal points. A non-ZCTA ZIP without an
+ authoritative coordinate remains missing.
+- Congressional districts come only from the Census relationship file.
+ USPS-only ZIPs that cannot be derived authoritatively remain unmapped with an
+ explicit quality reason; city-wide inference is prohibited.
+
+## Release contents and gate
+
+`06_finalize.R` creates under `data-raw/release/`:
+
+- `zipcodeR-data-VERSION.rds`, containing `zip_code_db`, `zcta_crosswalk`,
+ `zip_to_cd`, metadata, provenance, and quality sidecars;
+- `manifest-VERSION.json`, containing the release tag, bundle SHA256, source
+ URLs and SHA256 values, licenses, vintages, pipeline commit, R version,
+ dependency-lock checksum, schemas, row counts, and canonical output hashes;
+- `zipcodeR-reproducibility-VERSION.tar.gz`, containing the complete pipeline,
+ lock/container definitions, license notices, manifest, validation report,
+ and exact raw source archives.
+
+`data-raw/` is intentionally excluded from the CRAN package tarball to keep the
+runtime package small. The complete directory and raw inputs accompany every
+data release through the reproducibility archive instead.
+
+An asset is not publishable until all of these are true:
+
+1. the working tree and recorded pipeline commit are clean and reviewed;
+2. two clean pinned-environment rebuilds produce the same bundle, manifest,
+ reproducibility archive, and canonical output hashes;
+3. package compatibility, `_ng`, integrity, and platform checks pass;
+4. the release is public rather than draft, and every public URL and checksum
+ succeeds from a clean machine.
+
+The workflow in `.github/workflows/refresh-data.yaml` uploads unpublished
+candidates for review. It does not update package datasets, open an automatic
+data PR, select a version, or publish a release.
+
+## Licenses
+
+See `LICENSES.md`. Census material is U.S. public domain, GeoNames material is
+CC BY 4.0 with attribution, and uszipcode-project snapshots are MIT-licensed.
diff --git a/data-raw/fips_codes.R b/data-raw/fips_codes.R
new file mode 100644
index 0000000..71f1ad8
--- /dev/null
+++ b/data-raw/fips_codes.R
@@ -0,0 +1,36 @@
+# Regenerate the internal `fips_codes` table bundled in R/sysdata.rda
+#
+# Provenance: U.S. Census Bureau state and county FIPS codes (public
+# domain), as compiled in the `tidycensus` package (MIT license, Kyle
+# Walker), which builds the table from the Census reference files at
+# https://www.census.gov/library/reference/code-lists/ansi.html
+#
+# zipcodeR previously depended on the whole tidycensus package (and its
+# sf/GDAL dependency chain) solely to read this static 3,256-row table.
+# It is now vendored as internal data. Re-run this script to refresh it,
+# then rebuild the package.
+#
+# Requires: tidycensus (only at data-build time, never at runtime)
+
+fips_codes <- tidycensus::fips_codes
+stopifnot(
+ is.data.frame(fips_codes),
+ identical(
+ names(fips_codes),
+ c("state", "state_code", "state_name", "county_code", "county")
+ ),
+ nrow(fips_codes) > 3000
+)
+
+# sysdata.rda carries other internal objects (zip_code_db_version,
+# zip_data_meta, ...); load them all and re-save everything so nothing is
+# silently dropped
+sysdata_env <- new.env()
+load(file.path("R", "sysdata.rda"), envir = sysdata_env)
+assign("fips_codes", fips_codes, envir = sysdata_env)
+save(
+ list = ls(sysdata_env, all.names = TRUE),
+ envir = sysdata_env,
+ file = file.path("R", "sysdata.rda"),
+ compress = "bzip2"
+)
diff --git a/data-raw/pkg.lock b/data-raw/pkg.lock
new file mode 100644
index 0000000..c317b26
--- /dev/null
+++ b/data-raw/pkg.lock
@@ -0,0 +1,3522 @@
+{
+ "lockfile_version": 1,
+ "os": "macOS Tahoe 26.6.2",
+ "r_version": "R version 4.6.1 (2026-06-24)",
+ "platform": "aarch64-apple-darwin25.4.0",
+ "packages": [
+ {
+ "ref": "DBI@1.3.0",
+ "package": "DBI",
+ "version": "1.3.0",
+ "type": "standard",
+ "direct": true,
+ "binary": false,
+ "dependencies": [],
+ "deps": [
+ {
+ "ref": "arrow",
+ "type": "suggests",
+ "package": "arrow",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "blob",
+ "type": "suggests",
+ "package": "blob",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "callr",
+ "type": "suggests",
+ "package": "callr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "covr",
+ "type": "suggests",
+ "package": "covr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "DBItest",
+ "type": "suggests",
+ "package": "DBItest",
+ "op": ">=",
+ "version": "1.8.2"
+ },
+ {
+ "ref": "dbplyr",
+ "type": "suggests",
+ "package": "dbplyr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "downlit",
+ "type": "suggests",
+ "package": "downlit",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "dplyr",
+ "type": "suggests",
+ "package": "dplyr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "glue",
+ "type": "suggests",
+ "package": "glue",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "hms",
+ "type": "suggests",
+ "package": "hms",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "knitr",
+ "type": "suggests",
+ "package": "knitr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "magrittr",
+ "type": "suggests",
+ "package": "magrittr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "methods",
+ "type": "depends",
+ "package": "methods",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "nanoarrow",
+ "type": "suggests",
+ "package": "nanoarrow",
+ "op": ">=",
+ "version": "0.3.0.1"
+ },
+ {
+ "ref": "otel",
+ "type": "suggests",
+ "package": "otel",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "otelsdk",
+ "type": "suggests",
+ "package": "otelsdk",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "R",
+ "type": "depends",
+ "package": "R",
+ "op": ">=",
+ "version": "3.0.0"
+ },
+ {
+ "ref": "RMariaDB",
+ "type": "suggests",
+ "package": "RMariaDB",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "rmarkdown",
+ "type": "suggests",
+ "package": "rmarkdown",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "rprojroot",
+ "type": "suggests",
+ "package": "rprojroot",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "RSQLite",
+ "type": "suggests",
+ "package": "RSQLite",
+ "op": ">=",
+ "version": "1.1-2"
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": ">=",
+ "version": "3.0.0"
+ },
+ {
+ "ref": "vctrs",
+ "type": "suggests",
+ "package": "vctrs",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "xml2",
+ "type": "suggests",
+ "package": "xml2",
+ "op": "",
+ "version": ""
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": false,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "DBI@1.3.0",
+ "RemoteRef": "DBI@1.3.0",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "1.3.0"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/DBI_1.3.0.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/DBI/DBI_1.3.0.tar.gz"],
+ "target": "src/contrib/DBI_1.3.0.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": true,
+ "license": "LGPL (>= 2.1)",
+ "sha256": "13def8e90cbe41205a0dfcf585a6a7ea79ce10d45969789e82613c7ce3d5fb18",
+ "filesize": 744704,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "bit",
+ "package": "bit",
+ "version": "4.6.0",
+ "type": "standard",
+ "direct": false,
+ "binary": false,
+ "dependencies": [],
+ "deps": [
+ {
+ "ref": "bit64",
+ "type": "suggests",
+ "package": "bit64",
+ "op": ">=",
+ "version": "4.0.0"
+ },
+ {
+ "ref": "ff",
+ "type": "suggests",
+ "package": "ff",
+ "op": ">=",
+ "version": "4.0.0"
+ },
+ {
+ "ref": "knitr",
+ "type": "suggests",
+ "package": "knitr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "markdown",
+ "type": "suggests",
+ "package": "markdown",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "microbenchmark",
+ "type": "suggests",
+ "package": "microbenchmark",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "R",
+ "type": "depends",
+ "package": "R",
+ "op": ">=",
+ "version": "3.4.0"
+ },
+ {
+ "ref": "rmarkdown",
+ "type": "suggests",
+ "package": "rmarkdown",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "roxygen2",
+ "type": "suggests",
+ "package": "roxygen2",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": ">=",
+ "version": "3.0.0"
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": true,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "bit",
+ "RemoteRef": "bit",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "4.6.0"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/bit_4.6.0.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/bit/bit_4.6.0.tar.gz"],
+ "target": "src/contrib/bit_4.6.0.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": false,
+ "license": "GPL-2 | GPL-3",
+ "sha256": "48fe21c5d04c7b724d695eeb60074395c0c631a7fb234e2075de92471445de08",
+ "filesize": 304585,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "bit64",
+ "package": "bit64",
+ "version": "4.8.4",
+ "type": "standard",
+ "direct": false,
+ "binary": false,
+ "dependencies": ["bit"],
+ "deps": [
+ {
+ "ref": "bit",
+ "type": "imports",
+ "package": "bit",
+ "op": ">=",
+ "version": "4.0.0"
+ },
+ {
+ "ref": "graphics",
+ "type": "imports",
+ "package": "graphics",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "methods",
+ "type": "imports",
+ "package": "methods",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "patrick",
+ "type": "suggests",
+ "package": "patrick",
+ "op": ">=",
+ "version": "0.3.0"
+ },
+ {
+ "ref": "R",
+ "type": "depends",
+ "package": "R",
+ "op": ">=",
+ "version": "3.5.0"
+ },
+ {
+ "ref": "stats",
+ "type": "imports",
+ "package": "stats",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": ">=",
+ "version": "3.3.0"
+ },
+ {
+ "ref": "utils",
+ "type": "imports",
+ "package": "utils",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "withr",
+ "type": "suggests",
+ "package": "withr",
+ "op": "",
+ "version": ""
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": true,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "bit64",
+ "RemoteRef": "bit64",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "4.8.4"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/bit64_4.8.4.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/bit64/bit64_4.8.4.tar.gz"],
+ "target": "src/contrib/bit64_4.8.4.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": false,
+ "license": "GPL-2 | GPL-3",
+ "sha256": "1feb4384058b1510861e296f317ee9f43aca75b753065d2268317620f8c208bc",
+ "filesize": 179334,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "blob",
+ "package": "blob",
+ "version": "1.3.0",
+ "type": "standard",
+ "direct": false,
+ "binary": false,
+ "dependencies": ["rlang", "vctrs"],
+ "deps": [
+ {
+ "ref": "covr",
+ "type": "suggests",
+ "package": "covr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "crayon",
+ "type": "suggests",
+ "package": "crayon",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "methods",
+ "type": "imports",
+ "package": "methods",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "pillar",
+ "type": "suggests",
+ "package": "pillar",
+ "op": ">=",
+ "version": "1.2.1"
+ },
+ {
+ "ref": "rlang",
+ "type": "imports",
+ "package": "rlang",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": ">=",
+ "version": "3.0.0"
+ },
+ {
+ "ref": "vctrs",
+ "type": "imports",
+ "package": "vctrs",
+ "op": ">=",
+ "version": "0.2.1"
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": false,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "blob",
+ "RemoteRef": "blob",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "1.3.0"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/blob_1.3.0.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/blob/blob_1.3.0.tar.gz"],
+ "target": "src/contrib/blob_1.3.0.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": false,
+ "license": "MIT + file LICENSE",
+ "sha256": "d4d2aeb8d17683bbfc256a7541e282af9663a752988a21a6ba72a4ab268c7331",
+ "filesize": 11157,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "cachem",
+ "package": "cachem",
+ "version": "1.1.0",
+ "type": "standard",
+ "direct": false,
+ "binary": false,
+ "dependencies": ["fastmap", "rlang"],
+ "deps": [
+ {
+ "ref": "fastmap",
+ "type": "imports",
+ "package": "fastmap",
+ "op": ">=",
+ "version": "1.2.0"
+ },
+ {
+ "ref": "rlang",
+ "type": "imports",
+ "package": "rlang",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": "",
+ "version": ""
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": true,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "cachem",
+ "RemoteRef": "cachem",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "1.1.0"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/cachem_1.1.0.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/cachem/cachem_1.1.0.tar.gz"],
+ "target": "src/contrib/cachem_1.1.0.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": false,
+ "license": "MIT + file LICENSE",
+ "sha256": "550839fc2ae5d865db475ba2c1714144f07fa0c052c72135b0e4a70287492e21",
+ "filesize": 27192,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "cli",
+ "package": "cli",
+ "version": "3.6.6",
+ "type": "standard",
+ "direct": false,
+ "binary": false,
+ "dependencies": [],
+ "deps": [
+ {
+ "ref": "callr",
+ "type": "suggests",
+ "package": "callr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "covr",
+ "type": "suggests",
+ "package": "covr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "crayon",
+ "type": "suggests",
+ "package": "crayon",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "digest",
+ "type": "suggests",
+ "package": "digest",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "glue",
+ "type": "suggests",
+ "package": "glue",
+ "op": ">=",
+ "version": "1.6.0"
+ },
+ {
+ "ref": "grDevices",
+ "type": "suggests",
+ "package": "grDevices",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "htmltools",
+ "type": "suggests",
+ "package": "htmltools",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "htmlwidgets",
+ "type": "suggests",
+ "package": "htmlwidgets",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "knitr",
+ "type": "suggests",
+ "package": "knitr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "methods",
+ "type": "suggests",
+ "package": "methods",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "processx",
+ "type": "suggests",
+ "package": "processx",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "ps",
+ "type": "suggests",
+ "package": "ps",
+ "op": ">=",
+ "version": "1.3.4.9000"
+ },
+ {
+ "ref": "R",
+ "type": "depends",
+ "package": "R",
+ "op": ">=",
+ "version": "3.4"
+ },
+ {
+ "ref": "rlang",
+ "type": "suggests",
+ "package": "rlang",
+ "op": ">=",
+ "version": "1.0.2.9003"
+ },
+ {
+ "ref": "rmarkdown",
+ "type": "suggests",
+ "package": "rmarkdown",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "rprojroot",
+ "type": "suggests",
+ "package": "rprojroot",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "rstudioapi",
+ "type": "suggests",
+ "package": "rstudioapi",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": ">=",
+ "version": "3.2.0"
+ },
+ {
+ "ref": "tibble",
+ "type": "suggests",
+ "package": "tibble",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "utils",
+ "type": "imports",
+ "package": "utils",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "whoami",
+ "type": "suggests",
+ "package": "whoami",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "withr",
+ "type": "suggests",
+ "package": "withr",
+ "op": "",
+ "version": ""
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": true,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "cli",
+ "RemoteRef": "cli",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "3.6.6"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/cli_3.6.6.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/cli/cli_3.6.6.tar.gz"],
+ "target": "src/contrib/cli_3.6.6.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": false,
+ "license": "MIT + file LICENSE",
+ "sha256": "b2b58d6dd82f5798b335e39c00591686a01fd3e94399ef898e146173e36f18f9",
+ "filesize": 644134,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "cpp11",
+ "package": "cpp11",
+ "version": "0.5.5",
+ "type": "standard",
+ "direct": false,
+ "binary": false,
+ "dependencies": [],
+ "deps": [
+ {
+ "ref": "bench",
+ "type": "suggests",
+ "package": "bench",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "brio",
+ "type": "suggests",
+ "package": "brio",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "callr",
+ "type": "suggests",
+ "package": "callr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "cli",
+ "type": "suggests",
+ "package": "cli",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "covr",
+ "type": "suggests",
+ "package": "covr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "decor",
+ "type": "suggests",
+ "package": "decor",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "desc",
+ "type": "suggests",
+ "package": "desc",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "ggplot2",
+ "type": "suggests",
+ "package": "ggplot2",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "glue",
+ "type": "suggests",
+ "package": "glue",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "knitr",
+ "type": "suggests",
+ "package": "knitr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "lobstr",
+ "type": "suggests",
+ "package": "lobstr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "mockery",
+ "type": "suggests",
+ "package": "mockery",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "progress",
+ "type": "suggests",
+ "package": "progress",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "R",
+ "type": "depends",
+ "package": "R",
+ "op": ">=",
+ "version": "4.0.0"
+ },
+ {
+ "ref": "Rcpp",
+ "type": "suggests",
+ "package": "Rcpp",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "rmarkdown",
+ "type": "suggests",
+ "package": "rmarkdown",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "scales",
+ "type": "suggests",
+ "package": "scales",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": ">=",
+ "version": "3.2.0"
+ },
+ {
+ "ref": "tibble",
+ "type": "suggests",
+ "package": "tibble",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "utils",
+ "type": "suggests",
+ "package": "utils",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "vctrs",
+ "type": "suggests",
+ "package": "vctrs",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "withr",
+ "type": "suggests",
+ "package": "withr",
+ "op": "",
+ "version": ""
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": false,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "cpp11",
+ "RemoteRef": "cpp11",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "0.5.5"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/cpp11_0.5.5.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/cpp11/cpp11_0.5.5.tar.gz"],
+ "target": "src/contrib/cpp11_0.5.5.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": false,
+ "license": "MIT + file LICENSE",
+ "sha256": "72486beb0605c1229bf3d422cd536224af8dc09bd389a3116f62f4c7705c62f4",
+ "filesize": 304823,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "fastmap",
+ "package": "fastmap",
+ "version": "1.2.0",
+ "type": "standard",
+ "direct": false,
+ "binary": false,
+ "dependencies": [],
+ "deps": [
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": ">=",
+ "version": "2.1.1"
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": true,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "fastmap",
+ "RemoteRef": "fastmap",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "1.2.0"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/fastmap_1.2.0.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/fastmap/fastmap_1.2.0.tar.gz"],
+ "target": "src/contrib/fastmap_1.2.0.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": false,
+ "license": "MIT + file LICENSE",
+ "sha256": "b1da04a2915d1d057f3c2525e295ef15016a64e6667eac83a14641bbd83b9246",
+ "filesize": 46710,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "glue",
+ "package": "glue",
+ "version": "1.8.1",
+ "type": "standard",
+ "direct": false,
+ "binary": false,
+ "dependencies": [],
+ "deps": [
+ {
+ "ref": "crayon",
+ "type": "suggests",
+ "package": "crayon",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "DBI",
+ "type": "suggests",
+ "package": "DBI",
+ "op": ">=",
+ "version": "1.2.0"
+ },
+ {
+ "ref": "dplyr",
+ "type": "suggests",
+ "package": "dplyr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "knitr",
+ "type": "suggests",
+ "package": "knitr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "methods",
+ "type": "imports",
+ "package": "methods",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "R",
+ "type": "depends",
+ "package": "R",
+ "op": ">=",
+ "version": "4.1"
+ },
+ {
+ "ref": "rlang",
+ "type": "suggests",
+ "package": "rlang",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "rmarkdown",
+ "type": "suggests",
+ "package": "rmarkdown",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "RSQLite",
+ "type": "suggests",
+ "package": "RSQLite",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": ">=",
+ "version": "3.2.0"
+ },
+ {
+ "ref": "vctrs",
+ "type": "suggests",
+ "package": "vctrs",
+ "op": ">=",
+ "version": "0.3.0"
+ },
+ {
+ "ref": "waldo",
+ "type": "suggests",
+ "package": "waldo",
+ "op": ">=",
+ "version": "0.5.3"
+ },
+ {
+ "ref": "withr",
+ "type": "suggests",
+ "package": "withr",
+ "op": "",
+ "version": ""
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": true,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "glue",
+ "RemoteRef": "glue",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "1.8.1"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/glue_1.8.1.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/glue/glue_1.8.1.tar.gz"],
+ "target": "src/contrib/glue_1.8.1.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": false,
+ "license": "MIT + file LICENSE",
+ "sha256": "1c55905d3efc3d5c199ceb0bd12218e97f0d4c64df6038ff41ecef415478a122",
+ "filesize": 129786,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "lifecycle",
+ "package": "lifecycle",
+ "version": "1.0.5",
+ "type": "standard",
+ "direct": false,
+ "binary": false,
+ "dependencies": ["cli", "rlang"],
+ "deps": [
+ {
+ "ref": "cli",
+ "type": "imports",
+ "package": "cli",
+ "op": ">=",
+ "version": "3.4.0"
+ },
+ {
+ "ref": "covr",
+ "type": "suggests",
+ "package": "covr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "knitr",
+ "type": "suggests",
+ "package": "knitr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "lintr",
+ "type": "suggests",
+ "package": "lintr",
+ "op": ">=",
+ "version": "3.1.0"
+ },
+ {
+ "ref": "R",
+ "type": "depends",
+ "package": "R",
+ "op": ">=",
+ "version": "3.6"
+ },
+ {
+ "ref": "rlang",
+ "type": "imports",
+ "package": "rlang",
+ "op": ">=",
+ "version": "1.1.0"
+ },
+ {
+ "ref": "rmarkdown",
+ "type": "suggests",
+ "package": "rmarkdown",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": ">=",
+ "version": "3.0.1"
+ },
+ {
+ "ref": "tibble",
+ "type": "suggests",
+ "package": "tibble",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "tidyverse",
+ "type": "suggests",
+ "package": "tidyverse",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "tools",
+ "type": "suggests",
+ "package": "tools",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "vctrs",
+ "type": "suggests",
+ "package": "vctrs",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "withr",
+ "type": "suggests",
+ "package": "withr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "xml2",
+ "type": "suggests",
+ "package": "xml2",
+ "op": "",
+ "version": ""
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": false,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "lifecycle",
+ "RemoteRef": "lifecycle",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "1.0.5"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/lifecycle_1.0.5.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/lifecycle/lifecycle_1.0.5.tar.gz"],
+ "target": "src/contrib/lifecycle_1.0.5.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": false,
+ "license": "MIT + file LICENSE",
+ "sha256": "61841e3e6edba056a88355a3f1d6698ab8d5d9cb3c05f2af0ec5a44ab516f8ee",
+ "filesize": 107137,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "memoise",
+ "package": "memoise",
+ "version": "2.0.1",
+ "type": "standard",
+ "direct": false,
+ "binary": false,
+ "dependencies": ["cachem", "rlang"],
+ "deps": [
+ {
+ "ref": "aws.s3",
+ "type": "suggests",
+ "package": "aws.s3",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "cachem",
+ "type": "imports",
+ "package": "cachem",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "covr",
+ "type": "suggests",
+ "package": "covr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "digest",
+ "type": "suggests",
+ "package": "digest",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "googleAuthR",
+ "type": "suggests",
+ "package": "googleAuthR",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "googleCloudStorageR",
+ "type": "suggests",
+ "package": "googleCloudStorageR",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "httr",
+ "type": "suggests",
+ "package": "httr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "rlang",
+ "type": "imports",
+ "package": "rlang",
+ "op": ">=",
+ "version": "0.4.10"
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": "",
+ "version": ""
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": false,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "memoise",
+ "RemoteRef": "memoise",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "2.0.1"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/memoise_2.0.1.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/memoise/memoise_2.0.1.tar.gz"],
+ "target": "src/contrib/memoise_2.0.1.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": false,
+ "license": "MIT + file LICENSE",
+ "sha256": "f85034ee98c8ca07fb3cd826142c1cd1e1e5747075a94c75a45783bbc4fe2deb",
+ "filesize": 17852,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "pkgconfig",
+ "package": "pkgconfig",
+ "version": "2.0.3",
+ "type": "standard",
+ "direct": false,
+ "binary": false,
+ "dependencies": [],
+ "deps": [
+ {
+ "ref": "covr",
+ "type": "suggests",
+ "package": "covr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "disposables",
+ "type": "suggests",
+ "package": "disposables",
+ "op": ">=",
+ "version": "1.0.3"
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "utils",
+ "type": "imports",
+ "package": "utils",
+ "op": "",
+ "version": ""
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": false,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "pkgconfig",
+ "RemoteRef": "pkgconfig",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "2.0.3"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/pkgconfig_2.0.3.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/pkgconfig/pkgconfig_2.0.3.tar.gz"],
+ "target": "src/contrib/pkgconfig_2.0.3.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": false,
+ "license": "MIT + file LICENSE",
+ "sha256": "330fef440ffeb842a7dcfffc8303743f1feae83e8d6131078b5a44ff11bc3850",
+ "filesize": 6080,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "rlang",
+ "package": "rlang",
+ "version": "1.3.0",
+ "type": "standard",
+ "direct": false,
+ "binary": false,
+ "dependencies": [],
+ "deps": [
+ {
+ "ref": "cli",
+ "type": "suggests",
+ "package": "cli",
+ "op": ">=",
+ "version": "3.1.0"
+ },
+ {
+ "ref": "covr",
+ "type": "suggests",
+ "package": "covr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "crayon",
+ "type": "suggests",
+ "package": "crayon",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "desc",
+ "type": "suggests",
+ "package": "desc",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "fs",
+ "type": "suggests",
+ "package": "fs",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "glue",
+ "type": "suggests",
+ "package": "glue",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "knitr",
+ "type": "suggests",
+ "package": "knitr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "magrittr",
+ "type": "suggests",
+ "package": "magrittr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "methods",
+ "type": "suggests",
+ "package": "methods",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "pillar",
+ "type": "suggests",
+ "package": "pillar",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "pkgload",
+ "type": "suggests",
+ "package": "pkgload",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "R",
+ "type": "depends",
+ "package": "R",
+ "op": ">=",
+ "version": "4.0.0"
+ },
+ {
+ "ref": "rmarkdown",
+ "type": "suggests",
+ "package": "rmarkdown",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "stats",
+ "type": "suggests",
+ "package": "stats",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": ">=",
+ "version": "3.3.2"
+ },
+ {
+ "ref": "tibble",
+ "type": "suggests",
+ "package": "tibble",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "usethis",
+ "type": "suggests",
+ "package": "usethis",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "utils",
+ "type": "imports",
+ "package": "utils",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "vctrs",
+ "type": "suggests",
+ "package": "vctrs",
+ "op": ">=",
+ "version": "0.2.3"
+ },
+ {
+ "ref": "winch",
+ "type": "enhances",
+ "package": "winch",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "withr",
+ "type": "suggests",
+ "package": "withr",
+ "op": "",
+ "version": ""
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": true,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "rlang",
+ "RemoteRef": "rlang",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "1.3.0"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/rlang_1.3.0.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/rlang/rlang_1.3.0.tar.gz"],
+ "target": "src/contrib/rlang_1.3.0.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": false,
+ "license": "MIT + file LICENSE",
+ "sha256": "80aa537d49e72265f443a15e8936128af583caa5e572086518b503cbf064279c",
+ "filesize": 784080,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "RSQLite@3.53.3",
+ "package": "RSQLite",
+ "version": "3.53.3",
+ "type": "standard",
+ "direct": true,
+ "binary": false,
+ "dependencies": ["bit64", "blob", "cpp11", "DBI", "memoise", "pkgconfig", "rlang"],
+ "deps": [
+ {
+ "ref": "bit64",
+ "type": "imports",
+ "package": "bit64",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "blob",
+ "type": "imports",
+ "package": "blob",
+ "op": ">=",
+ "version": "1.2.0"
+ },
+ {
+ "ref": "callr",
+ "type": "suggests",
+ "package": "callr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "cli",
+ "type": "suggests",
+ "package": "cli",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "cpp11",
+ "type": "linkingto",
+ "package": "cpp11",
+ "op": ">=",
+ "version": "0.4.0"
+ },
+ {
+ "ref": "DBI",
+ "type": "imports",
+ "package": "DBI",
+ "op": ">=",
+ "version": "1.2.0"
+ },
+ {
+ "ref": "DBItest",
+ "type": "suggests",
+ "package": "DBItest",
+ "op": ">=",
+ "version": "1.8.0"
+ },
+ {
+ "ref": "decor",
+ "type": "suggests",
+ "package": "decor",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "gert",
+ "type": "suggests",
+ "package": "gert",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "gh",
+ "type": "suggests",
+ "package": "gh",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "hms",
+ "type": "suggests",
+ "package": "hms",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "httpuv",
+ "type": "suggests",
+ "package": "httpuv",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "knitr",
+ "type": "suggests",
+ "package": "knitr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "magrittr",
+ "type": "suggests",
+ "package": "magrittr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "memoise",
+ "type": "imports",
+ "package": "memoise",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "methods",
+ "type": "imports",
+ "package": "methods",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "pkgconfig",
+ "type": "imports",
+ "package": "pkgconfig",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "R",
+ "type": "depends",
+ "package": "R",
+ "op": ">=",
+ "version": "3.1.0"
+ },
+ {
+ "ref": "rlang",
+ "type": "imports",
+ "package": "rlang",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "rmarkdown",
+ "type": "suggests",
+ "package": "rmarkdown",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "rvest",
+ "type": "suggests",
+ "package": "rvest",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": ">=",
+ "version": "3.0.0"
+ },
+ {
+ "ref": "withr",
+ "type": "suggests",
+ "package": "withr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "xml2",
+ "type": "suggests",
+ "package": "xml2",
+ "op": "",
+ "version": ""
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": true,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "RSQLite@3.53.3",
+ "RemoteRef": "RSQLite@3.53.3",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "3.53.3"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/RSQLite_3.53.3.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/RSQLite/RSQLite_3.53.3.tar.gz"],
+ "target": "src/contrib/RSQLite_3.53.3.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": true,
+ "license": "LGPL (>= 2.1)",
+ "sha256": "82946aac5e2869da5fdaf57d0e4afeee3d4deac21d9eaeffb5f743273a93736c",
+ "filesize": 4413180,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "vctrs",
+ "package": "vctrs",
+ "version": "0.7.3",
+ "type": "standard",
+ "direct": false,
+ "binary": false,
+ "dependencies": ["cli", "glue", "lifecycle", "rlang"],
+ "deps": [
+ {
+ "ref": "bit64",
+ "type": "suggests",
+ "package": "bit64",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "cli",
+ "type": "imports",
+ "package": "cli",
+ "op": ">=",
+ "version": "3.4.0"
+ },
+ {
+ "ref": "covr",
+ "type": "suggests",
+ "package": "covr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "crayon",
+ "type": "suggests",
+ "package": "crayon",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "dplyr",
+ "type": "suggests",
+ "package": "dplyr",
+ "op": ">=",
+ "version": "0.8.5"
+ },
+ {
+ "ref": "generics",
+ "type": "suggests",
+ "package": "generics",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "glue",
+ "type": "imports",
+ "package": "glue",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "knitr",
+ "type": "suggests",
+ "package": "knitr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "lifecycle",
+ "type": "imports",
+ "package": "lifecycle",
+ "op": ">=",
+ "version": "1.0.3"
+ },
+ {
+ "ref": "pillar",
+ "type": "suggests",
+ "package": "pillar",
+ "op": ">=",
+ "version": "1.4.4"
+ },
+ {
+ "ref": "pkgdown",
+ "type": "suggests",
+ "package": "pkgdown",
+ "op": ">=",
+ "version": "2.0.1"
+ },
+ {
+ "ref": "R",
+ "type": "depends",
+ "package": "R",
+ "op": ">=",
+ "version": "4.0.0"
+ },
+ {
+ "ref": "rlang",
+ "type": "imports",
+ "package": "rlang",
+ "op": ">=",
+ "version": "1.1.7"
+ },
+ {
+ "ref": "rmarkdown",
+ "type": "suggests",
+ "package": "rmarkdown",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": ">=",
+ "version": "3.0.0"
+ },
+ {
+ "ref": "tibble",
+ "type": "suggests",
+ "package": "tibble",
+ "op": ">=",
+ "version": "3.1.3"
+ },
+ {
+ "ref": "waldo",
+ "type": "suggests",
+ "package": "waldo",
+ "op": ">=",
+ "version": "0.2.0"
+ },
+ {
+ "ref": "withr",
+ "type": "suggests",
+ "package": "withr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "xml2",
+ "type": "suggests",
+ "package": "xml2",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "zeallot",
+ "type": "suggests",
+ "package": "zeallot",
+ "op": "",
+ "version": ""
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": true,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "vctrs",
+ "RemoteRef": "vctrs",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "0.7.3"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/vctrs_0.7.3.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/vctrs/vctrs_0.7.3.tar.gz"],
+ "target": "src/contrib/vctrs_0.7.3.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": false,
+ "license": "MIT + file LICENSE",
+ "sha256": "b45078413e06ac624dddb7221a3a43908b405c8abec09822cb86638d30b0435b",
+ "filesize": 1083213,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "askpass",
+ "package": "askpass",
+ "version": "1.2.1",
+ "type": "standard",
+ "direct": false,
+ "binary": false,
+ "dependencies": ["sys"],
+ "deps": [
+ {
+ "ref": "sys",
+ "type": "imports",
+ "package": "sys",
+ "op": ">=",
+ "version": "2.1"
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": "",
+ "version": ""
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": true,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "askpass",
+ "RemoteRef": "askpass",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "1.2.1"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/askpass_1.2.1.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/askpass/askpass_1.2.1.tar.gz"],
+ "target": "src/contrib/askpass_1.2.1.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": false,
+ "license": "MIT + file LICENSE",
+ "sha256": "6c2106a74c44a748f2cea795d9686e27a0058a90debcfd8558b62b06aec0c7dd",
+ "filesize": 6058,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "openssl@2.4.2",
+ "package": "openssl",
+ "version": "2.4.2",
+ "type": "standard",
+ "direct": true,
+ "binary": false,
+ "dependencies": ["askpass"],
+ "deps": [
+ {
+ "ref": "askpass",
+ "type": "imports",
+ "package": "askpass",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "curl",
+ "type": "suggests",
+ "package": "curl",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "digest",
+ "type": "suggests",
+ "package": "digest",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "jose",
+ "type": "suggests",
+ "package": "jose",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "jsonlite",
+ "type": "suggests",
+ "package": "jsonlite",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "knitr",
+ "type": "suggests",
+ "package": "knitr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "rmarkdown",
+ "type": "suggests",
+ "package": "rmarkdown",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "sodium",
+ "type": "suggests",
+ "package": "sodium",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": ">=",
+ "version": "2.1.0"
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": true,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "openssl@2.4.2",
+ "RemoteRef": "openssl@2.4.2",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "2.4.2"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/openssl_2.4.2.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/openssl/openssl_2.4.2.tar.gz"],
+ "target": "src/contrib/openssl_2.4.2.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": true,
+ "license": "MIT + file LICENSE",
+ "sha256": "1fc030ad0a008472eac7f086666a7623bdbe6d27c5fdb07b5510f56545d2f4e7",
+ "filesize": 1177207,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": "OpenSSL >= 1.0.2"
+ },
+ {
+ "ref": "sys",
+ "package": "sys",
+ "version": "3.4.3",
+ "type": "standard",
+ "direct": false,
+ "binary": false,
+ "dependencies": [],
+ "deps": [
+ {
+ "ref": "spelling",
+ "type": "suggests",
+ "package": "spelling",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "unix",
+ "type": "suggests",
+ "package": "unix",
+ "op": ">=",
+ "version": "1.4"
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": true,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "sys",
+ "RemoteRef": "sys",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "3.4.3"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/sys_3.4.3.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/sys/sys_3.4.3.tar.gz"],
+ "target": "src/contrib/sys_3.4.3.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": false,
+ "license": "MIT + file LICENSE",
+ "sha256": "051e7332e3074db826efef9059067721864f9d70adc55bbcae3a72e5ae83913a",
+ "filesize": 19936,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "dplyr@1.2.1",
+ "package": "dplyr",
+ "version": "1.2.1",
+ "type": "standard",
+ "direct": true,
+ "binary": false,
+ "dependencies": ["cli", "generics", "glue", "lifecycle", "magrittr", "pillar", "R6", "rlang", "tibble", "tidyselect", "vctrs"],
+ "deps": [
+ {
+ "ref": "broom",
+ "type": "suggests",
+ "package": "broom",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "cli",
+ "type": "imports",
+ "package": "cli",
+ "op": ">=",
+ "version": "3.6.2"
+ },
+ {
+ "ref": "covr",
+ "type": "suggests",
+ "package": "covr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "DBI",
+ "type": "suggests",
+ "package": "DBI",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "dbplyr",
+ "type": "suggests",
+ "package": "dbplyr",
+ "op": ">=",
+ "version": "2.2.1"
+ },
+ {
+ "ref": "generics",
+ "type": "imports",
+ "package": "generics",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "ggplot2",
+ "type": "suggests",
+ "package": "ggplot2",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "glue",
+ "type": "imports",
+ "package": "glue",
+ "op": ">=",
+ "version": "1.3.2"
+ },
+ {
+ "ref": "knitr",
+ "type": "suggests",
+ "package": "knitr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "Lahman",
+ "type": "suggests",
+ "package": "Lahman",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "lifecycle",
+ "type": "imports",
+ "package": "lifecycle",
+ "op": ">=",
+ "version": "1.0.5"
+ },
+ {
+ "ref": "lobstr",
+ "type": "suggests",
+ "package": "lobstr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "magrittr",
+ "type": "imports",
+ "package": "magrittr",
+ "op": ">=",
+ "version": "1.5"
+ },
+ {
+ "ref": "methods",
+ "type": "imports",
+ "package": "methods",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "nycflights13",
+ "type": "suggests",
+ "package": "nycflights13",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "pillar",
+ "type": "imports",
+ "package": "pillar",
+ "op": ">=",
+ "version": "1.9.0"
+ },
+ {
+ "ref": "purrr",
+ "type": "suggests",
+ "package": "purrr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "R",
+ "type": "depends",
+ "package": "R",
+ "op": ">=",
+ "version": "4.1.0"
+ },
+ {
+ "ref": "R6",
+ "type": "imports",
+ "package": "R6",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "rlang",
+ "type": "imports",
+ "package": "rlang",
+ "op": ">=",
+ "version": "1.1.7"
+ },
+ {
+ "ref": "rmarkdown",
+ "type": "suggests",
+ "package": "rmarkdown",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "RSQLite",
+ "type": "suggests",
+ "package": "RSQLite",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "stringi",
+ "type": "suggests",
+ "package": "stringi",
+ "op": ">=",
+ "version": "1.7.6"
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": ">=",
+ "version": "3.1.5"
+ },
+ {
+ "ref": "tibble",
+ "type": "imports",
+ "package": "tibble",
+ "op": ">=",
+ "version": "3.2.0"
+ },
+ {
+ "ref": "tidyr",
+ "type": "suggests",
+ "package": "tidyr",
+ "op": ">=",
+ "version": "1.3.0"
+ },
+ {
+ "ref": "tidyselect",
+ "type": "imports",
+ "package": "tidyselect",
+ "op": ">=",
+ "version": "1.2.0"
+ },
+ {
+ "ref": "utils",
+ "type": "imports",
+ "package": "utils",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "vctrs",
+ "type": "imports",
+ "package": "vctrs",
+ "op": ">=",
+ "version": "0.7.1"
+ },
+ {
+ "ref": "withr",
+ "type": "suggests",
+ "package": "withr",
+ "op": "",
+ "version": ""
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": true,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "dplyr@1.2.1",
+ "RemoteRef": "dplyr@1.2.1",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "1.2.1"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/dplyr_1.2.1.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/dplyr/dplyr_1.2.1.tar.gz"],
+ "target": "src/contrib/dplyr_1.2.1.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": true,
+ "license": "MIT + file LICENSE",
+ "sha256": "18d66fe2f24cda8b619c177326d39dd015229ab99695fa67d3fa31e4569390e0",
+ "filesize": 923509,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "generics",
+ "package": "generics",
+ "version": "0.1.4",
+ "type": "standard",
+ "direct": false,
+ "binary": false,
+ "dependencies": [],
+ "deps": [
+ {
+ "ref": "covr",
+ "type": "suggests",
+ "package": "covr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "methods",
+ "type": "imports",
+ "package": "methods",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "pkgload",
+ "type": "suggests",
+ "package": "pkgload",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "R",
+ "type": "depends",
+ "package": "R",
+ "op": ">=",
+ "version": "3.6"
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": ">=",
+ "version": "3.0.0"
+ },
+ {
+ "ref": "tibble",
+ "type": "suggests",
+ "package": "tibble",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "withr",
+ "type": "suggests",
+ "package": "withr",
+ "op": "",
+ "version": ""
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": false,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "generics",
+ "RemoteRef": "generics",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "0.1.4"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/generics_0.1.4.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/generics/generics_0.1.4.tar.gz"],
+ "target": "src/contrib/generics_0.1.4.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": false,
+ "license": "MIT + file LICENSE",
+ "sha256": "bbe95a097792d38fc3b7e677738af1b95b66ea5e5017e33b8beac6a6088d0801",
+ "filesize": 47224,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "magrittr",
+ "package": "magrittr",
+ "version": "2.0.5",
+ "type": "standard",
+ "direct": false,
+ "binary": false,
+ "dependencies": [],
+ "deps": [
+ {
+ "ref": "covr",
+ "type": "suggests",
+ "package": "covr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "knitr",
+ "type": "suggests",
+ "package": "knitr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "R",
+ "type": "depends",
+ "package": "R",
+ "op": ">=",
+ "version": "3.4.0"
+ },
+ {
+ "ref": "rlang",
+ "type": "suggests",
+ "package": "rlang",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "rmarkdown",
+ "type": "suggests",
+ "package": "rmarkdown",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": "",
+ "version": ""
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": true,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "magrittr",
+ "RemoteRef": "magrittr",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "2.0.5"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/magrittr_2.0.5.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/magrittr/magrittr_2.0.5.tar.gz"],
+ "target": "src/contrib/magrittr_2.0.5.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": false,
+ "license": "MIT + file LICENSE",
+ "sha256": "3f5b5b1d4e6d12807a95c50b6c88cb1d8af2c5eb5213f08bfe58f278eca2ed23",
+ "filesize": 281813,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "pillar",
+ "package": "pillar",
+ "version": "1.11.1",
+ "type": "standard",
+ "direct": false,
+ "binary": false,
+ "dependencies": ["cli", "glue", "lifecycle", "rlang", "utf8", "vctrs"],
+ "deps": [
+ {
+ "ref": "bit64",
+ "type": "suggests",
+ "package": "bit64",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "cli",
+ "type": "imports",
+ "package": "cli",
+ "op": ">=",
+ "version": "2.3.0"
+ },
+ {
+ "ref": "DBI",
+ "type": "suggests",
+ "package": "DBI",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "debugme",
+ "type": "suggests",
+ "package": "debugme",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "DiagrammeR",
+ "type": "suggests",
+ "package": "DiagrammeR",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "dplyr",
+ "type": "suggests",
+ "package": "dplyr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "formattable",
+ "type": "suggests",
+ "package": "formattable",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "ggplot2",
+ "type": "suggests",
+ "package": "ggplot2",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "glue",
+ "type": "imports",
+ "package": "glue",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "knitr",
+ "type": "suggests",
+ "package": "knitr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "lifecycle",
+ "type": "imports",
+ "package": "lifecycle",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "lubridate",
+ "type": "suggests",
+ "package": "lubridate",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "nanotime",
+ "type": "suggests",
+ "package": "nanotime",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "nycflights13",
+ "type": "suggests",
+ "package": "nycflights13",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "palmerpenguins",
+ "type": "suggests",
+ "package": "palmerpenguins",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "rlang",
+ "type": "imports",
+ "package": "rlang",
+ "op": ">=",
+ "version": "1.0.2"
+ },
+ {
+ "ref": "rmarkdown",
+ "type": "suggests",
+ "package": "rmarkdown",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "scales",
+ "type": "suggests",
+ "package": "scales",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "stringi",
+ "type": "suggests",
+ "package": "stringi",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "survival",
+ "type": "suggests",
+ "package": "survival",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": ">=",
+ "version": "3.1.1"
+ },
+ {
+ "ref": "tibble",
+ "type": "suggests",
+ "package": "tibble",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "units",
+ "type": "suggests",
+ "package": "units",
+ "op": ">=",
+ "version": "0.7.2"
+ },
+ {
+ "ref": "utf8",
+ "type": "imports",
+ "package": "utf8",
+ "op": ">=",
+ "version": "1.1.0"
+ },
+ {
+ "ref": "utils",
+ "type": "imports",
+ "package": "utils",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "vctrs",
+ "type": "imports",
+ "package": "vctrs",
+ "op": ">=",
+ "version": "0.5.0"
+ },
+ {
+ "ref": "vdiffr",
+ "type": "suggests",
+ "package": "vdiffr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "withr",
+ "type": "suggests",
+ "package": "withr",
+ "op": "",
+ "version": ""
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": false,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "pillar",
+ "RemoteRef": "pillar",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "1.11.1"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/pillar_1.11.1.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/pillar/pillar_1.11.1.tar.gz"],
+ "target": "src/contrib/pillar_1.11.1.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": false,
+ "license": "MIT + file LICENSE",
+ "sha256": "056ce154238c9b5b8d5dcbcb52e1bc51d33870ce08c8a9ca9496478bd59f4653",
+ "filesize": 409508,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "R6",
+ "package": "R6",
+ "version": "2.6.1",
+ "type": "standard",
+ "direct": false,
+ "binary": false,
+ "dependencies": [],
+ "deps": [
+ {
+ "ref": "lobstr",
+ "type": "suggests",
+ "package": "lobstr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "R",
+ "type": "depends",
+ "package": "R",
+ "op": ">=",
+ "version": "3.6"
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": ">=",
+ "version": "3.0.0"
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": false,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "R6",
+ "RemoteRef": "R6",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "2.6.1"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/R6_2.6.1.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/R6/R6_2.6.1.tar.gz"],
+ "target": "src/contrib/R6_2.6.1.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": false,
+ "license": "MIT + file LICENSE",
+ "sha256": "59c6eba8b1b912eb7e104f65053235604be853425ee67c152ac4e86a1f2073b4",
+ "filesize": 64507,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "tibble",
+ "package": "tibble",
+ "version": "3.3.1",
+ "type": "standard",
+ "direct": false,
+ "binary": false,
+ "dependencies": ["cli", "lifecycle", "magrittr", "pillar", "pkgconfig", "rlang", "vctrs"],
+ "deps": [
+ {
+ "ref": "bench",
+ "type": "suggests",
+ "package": "bench",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "bit64",
+ "type": "suggests",
+ "package": "bit64",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "blob",
+ "type": "suggests",
+ "package": "blob",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "brio",
+ "type": "suggests",
+ "package": "brio",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "callr",
+ "type": "suggests",
+ "package": "callr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "cli",
+ "type": "imports",
+ "package": "cli",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "DiagrammeR",
+ "type": "suggests",
+ "package": "DiagrammeR",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "dplyr",
+ "type": "suggests",
+ "package": "dplyr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "evaluate",
+ "type": "suggests",
+ "package": "evaluate",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "formattable",
+ "type": "suggests",
+ "package": "formattable",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "ggplot2",
+ "type": "suggests",
+ "package": "ggplot2",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "here",
+ "type": "suggests",
+ "package": "here",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "hms",
+ "type": "suggests",
+ "package": "hms",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "htmltools",
+ "type": "suggests",
+ "package": "htmltools",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "knitr",
+ "type": "suggests",
+ "package": "knitr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "lifecycle",
+ "type": "imports",
+ "package": "lifecycle",
+ "op": ">=",
+ "version": "1.0.0"
+ },
+ {
+ "ref": "lubridate",
+ "type": "suggests",
+ "package": "lubridate",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "magrittr",
+ "type": "imports",
+ "package": "magrittr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "methods",
+ "type": "imports",
+ "package": "methods",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "nycflights13",
+ "type": "suggests",
+ "package": "nycflights13",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "pillar",
+ "type": "imports",
+ "package": "pillar",
+ "op": ">=",
+ "version": "1.8.1"
+ },
+ {
+ "ref": "pkgconfig",
+ "type": "imports",
+ "package": "pkgconfig",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "pkgload",
+ "type": "suggests",
+ "package": "pkgload",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "purrr",
+ "type": "suggests",
+ "package": "purrr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "R",
+ "type": "depends",
+ "package": "R",
+ "op": ">=",
+ "version": "3.4.0"
+ },
+ {
+ "ref": "rlang",
+ "type": "imports",
+ "package": "rlang",
+ "op": ">=",
+ "version": "1.0.2"
+ },
+ {
+ "ref": "rmarkdown",
+ "type": "suggests",
+ "package": "rmarkdown",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "stringi",
+ "type": "suggests",
+ "package": "stringi",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": ">=",
+ "version": "3.0.2"
+ },
+ {
+ "ref": "tidyr",
+ "type": "suggests",
+ "package": "tidyr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "utils",
+ "type": "imports",
+ "package": "utils",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "vctrs",
+ "type": "imports",
+ "package": "vctrs",
+ "op": ">=",
+ "version": "0.5.0"
+ },
+ {
+ "ref": "withr",
+ "type": "suggests",
+ "package": "withr",
+ "op": "",
+ "version": ""
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": true,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "tibble",
+ "RemoteRef": "tibble",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "3.3.1"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/tibble_3.3.1.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/tibble/tibble_3.3.1.tar.gz"],
+ "target": "src/contrib/tibble_3.3.1.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": false,
+ "license": "MIT + file LICENSE",
+ "sha256": "fb309f8a1939021b237c7e85ff7ad6e8ff5acd57a6230d220a452094b492b28f",
+ "filesize": 557135,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "tidyselect",
+ "package": "tidyselect",
+ "version": "1.2.1",
+ "type": "standard",
+ "direct": false,
+ "binary": false,
+ "dependencies": ["cli", "glue", "lifecycle", "rlang", "vctrs", "withr"],
+ "deps": [
+ {
+ "ref": "cli",
+ "type": "imports",
+ "package": "cli",
+ "op": ">=",
+ "version": "3.3.0"
+ },
+ {
+ "ref": "covr",
+ "type": "suggests",
+ "package": "covr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "crayon",
+ "type": "suggests",
+ "package": "crayon",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "dplyr",
+ "type": "suggests",
+ "package": "dplyr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "glue",
+ "type": "imports",
+ "package": "glue",
+ "op": ">=",
+ "version": "1.3.0"
+ },
+ {
+ "ref": "knitr",
+ "type": "suggests",
+ "package": "knitr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "lifecycle",
+ "type": "imports",
+ "package": "lifecycle",
+ "op": ">=",
+ "version": "1.0.3"
+ },
+ {
+ "ref": "magrittr",
+ "type": "suggests",
+ "package": "magrittr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "R",
+ "type": "depends",
+ "package": "R",
+ "op": ">=",
+ "version": "3.4"
+ },
+ {
+ "ref": "rlang",
+ "type": "imports",
+ "package": "rlang",
+ "op": ">=",
+ "version": "1.0.4"
+ },
+ {
+ "ref": "rmarkdown",
+ "type": "suggests",
+ "package": "rmarkdown",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "stringr",
+ "type": "suggests",
+ "package": "stringr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": ">=",
+ "version": "3.1.1"
+ },
+ {
+ "ref": "tibble",
+ "type": "suggests",
+ "package": "tibble",
+ "op": ">=",
+ "version": "2.1.3"
+ },
+ {
+ "ref": "vctrs",
+ "type": "imports",
+ "package": "vctrs",
+ "op": ">=",
+ "version": "0.5.2"
+ },
+ {
+ "ref": "withr",
+ "type": "imports",
+ "package": "withr",
+ "op": "",
+ "version": ""
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": true,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "tidyselect",
+ "RemoteRef": "tidyselect",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "1.2.1"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/tidyselect_1.2.1.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/tidyselect/tidyselect_1.2.1.tar.gz"],
+ "target": "src/contrib/tidyselect_1.2.1.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": false,
+ "license": "MIT + file LICENSE",
+ "sha256": "169e97ba0bbfbcdf4a80534322751f87a04370310c40e27f04aac6525d45903c",
+ "filesize": 103591,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "utf8",
+ "package": "utf8",
+ "version": "1.2.6",
+ "type": "standard",
+ "direct": false,
+ "binary": false,
+ "dependencies": [],
+ "deps": [
+ {
+ "ref": "cli",
+ "type": "suggests",
+ "package": "cli",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "covr",
+ "type": "suggests",
+ "package": "covr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "knitr",
+ "type": "suggests",
+ "package": "knitr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "R",
+ "type": "depends",
+ "package": "R",
+ "op": ">=",
+ "version": "2.10"
+ },
+ {
+ "ref": "rlang",
+ "type": "suggests",
+ "package": "rlang",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "rmarkdown",
+ "type": "suggests",
+ "package": "rmarkdown",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": ">=",
+ "version": "3.0.0"
+ },
+ {
+ "ref": "withr",
+ "type": "suggests",
+ "package": "withr",
+ "op": "",
+ "version": ""
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": true,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "utf8",
+ "RemoteRef": "utf8",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "1.2.6"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/utf8_1.2.6.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/utf8/utf8_1.2.6.tar.gz"],
+ "target": "src/contrib/utf8_1.2.6.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": false,
+ "license": "Apache License (== 2.0) | file LICENSE",
+ "sha256": "4589f8b72291329e70b7f3a8c20f2feb4e7764eebad2e6976bc9a3eee7686ce9",
+ "filesize": 243856,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "withr",
+ "package": "withr",
+ "version": "3.0.3",
+ "type": "standard",
+ "direct": false,
+ "binary": false,
+ "dependencies": [],
+ "deps": [
+ {
+ "ref": "callr",
+ "type": "suggests",
+ "package": "callr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "DBI",
+ "type": "suggests",
+ "package": "DBI",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "graphics",
+ "type": "imports",
+ "package": "graphics",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "grDevices",
+ "type": "imports",
+ "package": "grDevices",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "knitr",
+ "type": "suggests",
+ "package": "knitr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "methods",
+ "type": "suggests",
+ "package": "methods",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "R",
+ "type": "depends",
+ "package": "R",
+ "op": ">=",
+ "version": "3.6.0"
+ },
+ {
+ "ref": "rlang",
+ "type": "suggests",
+ "package": "rlang",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "rmarkdown",
+ "type": "suggests",
+ "package": "rmarkdown",
+ "op": ">=",
+ "version": "2.12"
+ },
+ {
+ "ref": "RSQLite",
+ "type": "suggests",
+ "package": "RSQLite",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": ">=",
+ "version": "3.0.0"
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": false,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "withr",
+ "RemoteRef": "withr",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "3.0.3"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/withr_3.0.3.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/withr/withr_3.0.3.tar.gz"],
+ "target": "src/contrib/withr_3.0.3.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": false,
+ "license": "MIT + file LICENSE",
+ "sha256": "d86c6454164fc85678bfc85e2a6cce42f7461d45fa9a87de71f376e7956c991e",
+ "filesize": 103968,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ },
+ {
+ "ref": "jsonlite@2.0.0",
+ "package": "jsonlite",
+ "version": "2.0.0",
+ "type": "standard",
+ "direct": true,
+ "binary": false,
+ "dependencies": [],
+ "deps": [
+ {
+ "ref": "httr",
+ "type": "suggests",
+ "package": "httr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "knitr",
+ "type": "suggests",
+ "package": "knitr",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "methods",
+ "type": "depends",
+ "package": "methods",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "R.rsp",
+ "type": "suggests",
+ "package": "R.rsp",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "rmarkdown",
+ "type": "suggests",
+ "package": "rmarkdown",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "sf",
+ "type": "suggests",
+ "package": "sf",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "testthat",
+ "type": "suggests",
+ "package": "testthat",
+ "op": "",
+ "version": ""
+ },
+ {
+ "ref": "vctrs",
+ "type": "suggests",
+ "package": "vctrs",
+ "op": "",
+ "version": ""
+ }
+ ],
+ "vignettes": false,
+ "needscompilation": true,
+ "metadata": {
+ "RemoteType": "standard",
+ "RemotePkgRef": "jsonlite@2.0.0",
+ "RemoteRef": "jsonlite@2.0.0",
+ "RemoteRepos": "https://cran.rstudio.com",
+ "RemotePkgPlatform": "source",
+ "RemoteSha": "2.0.0"
+ },
+ "sources": ["https://cran.rstudio.com/src/contrib/jsonlite_2.0.0.tar.gz", "https://cran.rstudio.com/src/contrib/Archive/jsonlite/jsonlite_2.0.0.tar.gz"],
+ "target": "src/contrib/jsonlite_2.0.0.tar.gz",
+ "platform": "source",
+ "rversion": "*",
+ "directpkg": true,
+ "license": "MIT + file LICENSE",
+ "sha256": "75eb910c82b350ec33f094779da0f87bff154c232e4ae39c9896a9b89f3ac82d",
+ "filesize": 1055849,
+ "dep_types": ["Depends", "Imports", "LinkingTo"],
+ "params": [],
+ "install_args": [],
+ "repotype": "cran",
+ "sysreqs": ""
+ }
+ ]
+}
diff --git a/data-raw/refresh_sources.R b/data-raw/refresh_sources.R
new file mode 100644
index 0000000..c258d54
--- /dev/null
+++ b/data-raw/refresh_sources.R
@@ -0,0 +1,85 @@
+# Inspect upstream bytes for a proposed future data version. This mode never
+# builds datasets, mutates source pins, or reuses an existing release identity.
+# A maintainer reviews proposed-sources.json, updates sources.R deliberately,
+# and only then runs the deterministic rebuild pipeline.
+
+if (!identical(Sys.getenv("PIPELINE_MODE"), "refresh")) {
+ stop("Set PIPELINE_MODE=refresh to inspect upstream source bytes.")
+}
+proposed_version <- Sys.getenv("PIPELINE_PROPOSED_VERSION")
+if (!nzchar(proposed_version) || proposed_version %in% c("latest", "current", "stable")) {
+ stop("PIPELINE_PROPOSED_VERSION must be a new explicit immutable version.")
+}
+if (!identical(Sys.getenv("PIPELINE_DATA_VERSION"), proposed_version)) {
+ stop("PIPELINE_DATA_VERSION must equal PIPELINE_PROPOSED_VERSION in refresh mode.")
+}
+
+source(file.path("data-raw", "sources.R"))
+out_dir <- file.path("data-raw", "refresh-candidate", proposed_version)
+dir.create(out_dir, recursive = TRUE, showWarnings = FALSE)
+
+sha256_file <- function(path) {
+ if (exists("sha256sum", envir = asNamespace("tools"), inherits = FALSE)) {
+ return(unname(tools::sha256sum(path)))
+ }
+ con <- file(path, "rb")
+ on.exit(close(con), add = TRUE)
+ unclass(as.character(openssl::sha256(con)))
+}
+
+download_candidate <- function(id, source) {
+ destination <- file.path(out_dir, basename(source$url))
+ utils::download.file(source$url, destination, mode = "wb", quiet = TRUE)
+ list(
+ id = id,
+ file = basename(destination),
+ url = source$url,
+ configured_sha256 = source$sha256,
+ proposed_sha256 = sha256_file(destination),
+ changed = !identical(sha256_file(destination), source$sha256),
+ license = source$license
+ )
+}
+
+proposal <- Map(download_candidate, names(PIPELINE_SOURCES), PIPELINE_SOURCES)
+
+key <- Sys.getenv("CENSUS_API_KEY")
+if (!nzchar(key)) {
+ stop("CENSUS_API_KEY is required to inspect a proposed ACS response.")
+}
+acs_url <- paste0(
+ ACS_ENDPOINT, "?get=", paste(ACS_VARIABLES, collapse = ","),
+ "&for=zip%20code%20tabulation%20area:*&key=",
+ utils::URLencode(key, reserved = TRUE)
+)
+acs_path <- file.path(out_dir, sprintf("acs5_%d_zcta.json", ACS_VINTAGE))
+tryCatch(
+ utils::download.file(acs_url, acs_path, mode = "wb", quiet = TRUE),
+ error = function(e) {
+ message_text <- gsub(key, "", conditionMessage(e), fixed = TRUE)
+ stop("ACS refresh download failed: ", message_text, call. = FALSE)
+ }
+)
+proposal[[length(proposal) + 1L]] <- list(
+ id = "acs_response",
+ file = basename(acs_path),
+ url = ACS_ENDPOINT,
+ configured_sha256 = ACS_RESPONSE_SHA256,
+ proposed_sha256 = sha256_file(acs_path),
+ changed = !identical(sha256_file(acs_path), ACS_RESPONSE_SHA256),
+ license = "U.S. public domain (U.S. Census Bureau)",
+ vintage = ACS_VINTAGE
+)
+
+jsonlite::write_json(
+ list(
+ format = 1,
+ proposed_data_version = proposed_version,
+ created_at = Sys.getenv("PIPELINE_BUILD_TIMESTAMP"),
+ sources = proposal
+ ),
+ file.path(out_dir, "proposed-sources.json"),
+ auto_unbox = TRUE,
+ pretty = TRUE
+)
+message("source-refresh proposal written to ", out_dir)
diff --git a/data-raw/refresh_summary.md b/data-raw/refresh_summary.md
new file mode 100644
index 0000000..249d1f6
--- /dev/null
+++ b/data-raw/refresh_summary.md
@@ -0,0 +1,12 @@
+## Data refresh summary
+
+- `zip_code_db`: 41900 rows (was 41877): **23 added, 0 removed**
+ - added by type: PO Box (8), Standard (15)
+ - coordinates refreshed for 32907 existing ZIPs; ACS attributes refreshed for 33637 ZIPs
+- `zcta_crosswalk`: 168212 rows, 2020 ZCTA/tract vintage (previously 148897 rows)
+- `zip_to_cd`: 40116 rows, 119th-Congress vintage (previously 45914 rows)
+ - 33791 authoritative ZCTA-mapped ZIPs; 8109 ZIPs intentionally unmapped; no city/state-derived assignments
+ - 5631 pre-2020 legacy mappings not carried into the authoritative-only crosswalk
+- state-modal timezone imputed for 0 new ZIP(s)
+
+Candidate data validation gate: **passed**
diff --git a/data-raw/run_pipeline.R b/data-raw/run_pipeline.R
new file mode 100644
index 0000000..3775f57
--- /dev/null
+++ b/data-raw/run_pipeline.R
@@ -0,0 +1,30 @@
+# Orchestrator for the zipcodeR data pipeline. Run from the package root:
+#
+# PIPELINE_MODE=rebuild PIPELINE_DATA_VERSION=2026.08 \
+# PIPELINE_BUILD_TIMESTAMP=2026-08-24T00:00:00Z \
+# Rscript data-raw/run_pipeline.R
+#
+# See data-raw/README.md. CENSUS_API_KEY is needed only when the archived raw
+# ACS response is absent.
+
+if (!identical(Sys.getenv("PIPELINE_MODE"), "rebuild")) {
+ stop(
+ "run_pipeline.R only performs deterministic rebuilds. Set ",
+ "PIPELINE_MODE=rebuild, or run data-raw/refresh_sources.R to inspect ",
+ "upstream sources for a future version."
+ )
+}
+
+steps <- c(
+ "01_acquire.R",
+ "02_build_zip_code_db.R",
+ "03_build_zcta_crosswalk.R",
+ "04_build_zip_to_cd.R",
+ "05_validate.R",
+ "06_finalize.R"
+)
+for (step in steps) {
+ message("== ", step, " ==")
+ source(file.path("data-raw", step))
+}
+message("pipeline complete. Review data-raw/refresh_summary.md and data-raw/release/.")
diff --git a/data-raw/sources.R b/data-raw/sources.R
new file mode 100644
index 0000000..b962f9c
--- /dev/null
+++ b/data-raw/sources.R
@@ -0,0 +1,87 @@
+# Source registry for the zipcodeR data pipeline.
+#
+# Every static source is pinned by URL and SHA256. When a source publisher
+# updates a file in place (Census relationship files are stable; Gazetteer and
+# GeoNames get new vintages), update the URL/sha256 here and record the change
+# in the data release notes. The exact Census ACS response is also archived
+# and checksummed; its endpoint, variables, geography, and vintage are pinned.
+
+PIPELINE_SOURCES <- list(
+ zcta_tract_rel = list(
+ description = "Census 2020 ZCTA-to-tract national relationship file",
+ url = "https://www2.census.gov/geo/docs/maps-data/data/rel2020/zcta520/tab20_zcta520_tract20_natl.txt",
+ sha256 = "6a25d8c3fff4cf612c4d2dccc2c0cd6cb5bc99b807ff3d5d107a2e9b9d68dde0",
+ license = "U.S. public domain (U.S. Census Bureau)"
+ ),
+ zcta_county_rel = list(
+ description = "Census 2020 ZCTA-to-county national relationship file",
+ url = "https://www2.census.gov/geo/docs/maps-data/data/rel2020/zcta520/tab20_zcta520_county20_natl.txt",
+ sha256 = "3ed41278d637dc249e0323306f68be8a6c234e3090f4de88ef328dee71aeaaaf",
+ license = "U.S. public domain (U.S. Census Bureau)"
+ ),
+ cd_zcta_rel = list(
+ description = "Census 119th Congressional District-to-ZCTA national relationship file",
+ url = "https://www2.census.gov/geo/docs/maps-data/data/rel2020/cd-sld/tab20_cd11920_zcta520_natl.txt",
+ sha256 = "57fad59f65af5179ddd18dcfb8f72482dc0cf04fe26e2b9b2b34c51c04405f77",
+ license = "U.S. public domain (U.S. Census Bureau)"
+ ),
+ gazetteer_zcta = list(
+ description = "Census 2024 Gazetteer, national ZCTA file (2020 ZCTAs)",
+ url = "https://www2.census.gov/geo/docs/maps-data/data/gazetteer/2024_Gazetteer/2024_Gaz_zcta_national.zip",
+ sha256 = "7b85c04a131672f58b38a950eb82855d5fd4054f0bd9bc3f69aa28897a714e3d",
+ license = "U.S. public domain (U.S. Census Bureau)"
+ ),
+ geonames_us = list(
+ description = "GeoNames U.S. postal codes (place names, admin areas, coordinates)",
+ url = "https://download.geonames.org/export/zip/US.zip",
+ # GeoNames regenerates this URL in place. Published releases therefore pin
+ # and archive the exact downloaded bytes; a refresh requires a deliberate
+ # checksum update and creates a new data version.
+ sha256 = "34bf4144bf1231c2da500127bbbf7020920bb4331de403b5d850b77f45a8f509",
+ license = "CC BY 4.0 (GeoNames) - attribution required, kept in data docs"
+ ),
+ uszipcode_simple_101 = list(
+ description = "uszipcode-project 1.0.1 simple_db.sqlite (validation reference + row source for post-2021-06 additions)",
+ url = "https://github.com/MacHu-GWU/uszipcode-project/releases/download/1.0.1.db/simple_db.sqlite",
+ sha256 = "43383f108ef14dccd925107bc77705f622b1014111ad5c9e5e2a6837bb7f64ff",
+ license = "MIT (MacHu-GWU/uszipcode-project) - attribution kept in data docs"
+ )
+)
+
+# The ACS request is archived and checksummed just like a static source. The
+# endpoint and vintage document how a maintainer creates a deliberately new
+# archive; deterministic rebuilds consume the archived bytes.
+ACS_VINTAGE <- 2023 # ACS 5-year estimates, 2019-2023
+ACS_ENDPOINT <- sprintf("https://api.census.gov/data/%d/acs/acs5", ACS_VINTAGE)
+ACS_RESPONSE_SHA256 <- "e3abe3892e69d907d179a01f1c605372425cedaab59defba5e337770467db50c"
+ACS_DERIVED_SHA256 <- "93579acf61ca194170990f720b1fbdc7e1d0d0dbf7696fa502e83633bd02deb4"
+ACS_VARIABLES <- c(
+ population = "B01003_001E",
+ housing_units = "B25001_001E",
+ occupied_housing_units = "B25002_002E",
+ median_home_value = "B25077_001E",
+ median_household_income = "B19013_001E"
+)
+
+# A published data identity is always explicit. Dates and aliases such as
+# "latest" are deliberately rejected because the same research script must
+# resolve to the same bytes indefinitely.
+DATA_VERSION <- Sys.getenv("PIPELINE_DATA_VERSION")
+if (!nzchar(DATA_VERSION)) {
+ stop("PIPELINE_DATA_VERSION must be set to an explicit version such as 2026.08.")
+}
+if (DATA_VERSION %in% c("latest", "current", "stable")) {
+ stop("PIPELINE_DATA_VERSION must be immutable, not an alias such as 'latest'.")
+}
+
+# The comprehensive-database release asset that download_comprehensive_data()
+# should fetch. This is pinned EXPLICITLY - not derived from DATA_VERSION -
+# because the comprehensive asset is republished less often than the bundled
+# data refreshes. When (and only when) a new comprehensive asset is uploaded
+# to a data release, update all three fields together; 05_validate.R checks
+# their consistency.
+COMPREHENSIVE_RELEASE <- list(
+ release_tag = "data-2026.08",
+ asset = "comprehensive_db.sqlite",
+ sha256 = "d85ed4e25884bc27bdd339d57dd9e2d1763531d4c050acb7a05a3d5aca90668d"
+)
diff --git a/data-raw/supplemental_zips.csv b/data-raw/supplemental_zips.csv
new file mode 100644
index 0000000..d0bdbc1
--- /dev/null
+++ b/data-raw/supplemental_zips.csv
@@ -0,0 +1,2 @@
+zipcode,zipcode_type,major_city,state,county,lat,lng,source,note
+91230,PO Box,Glendale,CA,Los Angeles County,,,"Unverified issue report retained only as a quarantine lead","USPS-only ZIP with no Census ZCTA. No proxy centroid is assigned; publish only after authoritative ZIP validation and record-level provenance."
diff --git a/data-raw/vendor/pak_0.11.1.tar.gz b/data-raw/vendor/pak_0.11.1.tar.gz
new file mode 100644
index 0000000..efef7aa
Binary files /dev/null and b/data-raw/vendor/pak_0.11.1.tar.gz differ
diff --git a/inst/CITATION b/inst/CITATION
index f5fea67..430f715 100644
--- a/inst/CITATION
+++ b/inst/CITATION
@@ -1,14 +1,15 @@
citHeader("To cite zipcodeR in publications, please use:")
-citEntry(entry = "Article",
- author = "Gavin C. Rozzi",
- title = "zipcodeR: Advancing the analysis of spatial data at the ZIP code level in R",
- journal = "Software Impacts",
- issn = "2665-9638",
- volume = "9",
- pages = "100099",
- year = "2021",
- doi = "10.1016/j.simpa.2021.100099",
- url = "https://www.sciencedirect.com/science/article/pii/S2665963821000373/",
- textVersion = "G.C. Rozzi, zipcodeR: Advancing the analysis of spatial data at the ZIP code level in R, Softw. Impacts. (2021) 100099."
+bibentry(
+ bibtype = "Article",
+ author = "Gavin C. Rozzi",
+ title = "zipcodeR: Advancing the analysis of spatial data at the ZIP code level in R",
+ journal = "Software Impacts",
+ issn = "2665-9638",
+ volume = "9",
+ pages = "100099",
+ year = "2021",
+ doi = "10.1016/j.simpa.2021.100099",
+ url = "https://www.sciencedirect.com/science/article/pii/S2665963821000373/",
+ textVersion = "G.C. Rozzi, zipcodeR: Advancing the analysis of spatial data at the ZIP code level in R, Softw. Impacts. (2021) 100099."
)
diff --git a/issue_comments/issue-13.md b/issue_comments/issue-13.md
new file mode 100644
index 0000000..8339e33
--- /dev/null
+++ b/issue_comments/issue-13.md
@@ -0,0 +1,11 @@
+
+
+Following up on an old one: this error (`object 'zip_code_db' not found` from a
+lookup function) indicates a broken or partial installation — the datasets are
+lazy-loaded with the package, and this symptom typically appears after migrating a
+package library between R versions or an interrupted install. A clean
+`install.packages("zipcodeR")` resolves it.
+
+The 0.4.0 FAQ vignette (`vignette("faq", package = "zipcodeR")`) now documents this.
+Closing as stale/documented — please open a fresh issue if it recurs on a current
+release with a clean install.
diff --git a/issue_comments/issue-14.md b/issue_comments/issue-14.md
new file mode 100644
index 0000000..21913d5
--- /dev/null
+++ b/issue_comments/issue-14.md
@@ -0,0 +1,11 @@
+
+
+Thanks for the kind words! The negative longitudes are correct: the United States is
+in the western hemisphere, and the standard geographic sign convention makes
+longitudes west of the prime meridian negative. Mapping libraries and spatial packages
+all expect this, so please don't multiply by -1 — a positive 74° longitude would place
+a New Jersey ZIP code in Central Asia.
+
+The 0.4.0 release documents this in `?geocode_zip` and in a new FAQ vignette
+(`vignette("faq", package = "zipcodeR")`), so closing this as resolved by
+documentation.
diff --git a/issue_comments/issue-19.md b/issue_comments/issue-19.md
new file mode 100644
index 0000000..03882ac
--- /dev/null
+++ b/issue_comments/issue-19.md
@@ -0,0 +1,12 @@
+
+
+The package's default database is intentionally still the 2021-06-08 snapshot
+in 0.4.0. Changing it under existing function names changed results for most
+records and would invalidate reproducible analyses.
+
+0.4.0 instead adds `zip_data_version()` and an explicit, checksum-pinned data
+bundle API. The public `2026.08` bundle contains refreshed authoritative ZCTA
+coordinates and can be selected with `download_zip_data_bundle("2026.08")`,
+then used through `_ng` functions without changing old scripts. ZIPs without
+authoritative coordinates remain unavailable rather than receiving city-center
+proxy coordinates.
diff --git a/issue_comments/issue-20.md b/issue_comments/issue-20.md
new file mode 100644
index 0000000..8629679
--- /dev/null
+++ b/issue_comments/issue-20.md
@@ -0,0 +1,15 @@
+
+
+Thanks for the detailed report and reprexes. The ordering fix shipped in 0.3.4,
+and 0.4.0 adds an isolated differential test that locks the existing behavior
+to the 0.3.5 contract. In particular, the legacy WGS84 calculation remains
+unchanged:
+
+```r
+zip_distance("08731", "08901")
+# distance: 40.70 miles
+```
+
+The opt-in `zip_distance_ng(bundle, ...)` function preserves duplicates and
+input order while using the bundle's coordinates and modern haversine method.
+It is deliberately a separate name so an old analysis never changes silently.
diff --git a/issue_comments/issue-21.md b/issue_comments/issue-21.md
new file mode 100644
index 0000000..dccd799
--- /dev/null
+++ b/issue_comments/issue-21.md
@@ -0,0 +1,12 @@
+
+
+The stale `raster`/`terra` class-registration failure is real. In 0.4.0,
+`raster` remains an installed dependency so legacy distance functions retain
+their exact WGS84 results, but its namespace is no longer loaded by
+`library(zipcodeR)`. It is loaded lazily only if a legacy distance function is
+called. A clean-process installation test confirms that `zipcodeR` loads
+without loading `raster`, `terra`, `sp`, `sf`, or `tidycensus`.
+
+New analyses can use the `_ng` distance functions, which do not invoke
+`raster`. This resolves the reported package-load failure without silently
+changing prior numerical results.
diff --git a/issue_comments/issue-24.md b/issue_comments/issue-24.md
new file mode 100644
index 0000000..1bdc893
--- /dev/null
+++ b/issue_comments/issue-24.md
@@ -0,0 +1,11 @@
+
+
+The warnings came from eagerly loading the legacy spatial dependency graph,
+not from a ZIP lookup itself. In 0.4.0, `library(zipcodeR)` no longer loads
+`tidycensus`, `sf`, `raster`, `terra`, or their GDAL bindings. The legacy
+packages remain installed dependencies for compatibility and are loaded only
+when a legacy function that actually needs one is called. The `_ng` FIPS path
+uses the vendored static FIPS table and does not invoke `tidycensus`.
+
+This isolates package startup from a system GDAL/Arrow mismatch while keeping
+legacy calls and installation requirements reproducible.
diff --git a/issue_comments/issue-25.md b/issue_comments/issue-25.md
new file mode 100644
index 0000000..d51614e
--- /dev/null
+++ b/issue_comments/issue-25.md
@@ -0,0 +1,8 @@
+
+
+97003 is not added to the default 0.4.0 database because that object remains
+byte-identical to 0.3.5 for reproducibility. The deterministic data pipeline
+corroborates 97003 against the Census ZCTA Gazetteer, and the public
+checksum-verified `2026.08` bundle includes its Census internal point. In 0.4.0
+it is available through `reverse_zipcode_ng(bundle, "97003")` without changing
+legacy calls.
diff --git a/issue_comments/issue-26.md b/issue_comments/issue-26.md
new file mode 100644
index 0000000..d8a25f7
--- /dev/null
+++ b/issue_comments/issue-26.md
@@ -0,0 +1,13 @@
+
+
+The report appears to transpose two digits: `92130` is the San Diego ZIP and is
+present in both the historical database and the modern pipeline. `91230` is not
+present in the pinned Census ZCTA Gazetteer or ACS ZCTA response. A third-party
+source described it as a Glendale P.O. Box ZIP, but that is not sufficient
+evidence for a Census-derived centroid or district.
+
+The new pipeline therefore records `91230` as a quarantined lead rather than
+publishing a city-center proxy point. It can enter a future versioned bundle
+only after authoritative USPS validation, with an explicit coordinate method
+and quality status. This answers the San Diego case without inventing spatial
+data for a different identifier.
diff --git a/issue_comments/issue-27.md b/issue_comments/issue-27.md
new file mode 100644
index 0000000..ed84704
--- /dev/null
+++ b/issue_comments/issue-27.md
@@ -0,0 +1,9 @@
+
+
+The reported duplicate/order behavior is part of the installed 0.3.5 contract,
+so changing `reverse_zipcode()` or `geocode_zip()` in 0.4.0 would silently
+change existing research. Those names intentionally remain unchanged.
+
+0.4.0 adds `reverse_zipcode_ng(bundle, ...)` and
+`geocode_zip_ng(bundle, ...)`, which preserve input order and duplicates and
+return explicit missing rows. The separate suffix makes the correction opt-in.
diff --git a/issue_comments/issue-28.md b/issue_comments/issue-28.md
new file mode 100644
index 0000000..eb41c98
--- /dev/null
+++ b/issue_comments/issue-28.md
@@ -0,0 +1,10 @@
+
+
+0.4.0 retains `raster` for the frozen distance API but no longer imports it at
+package startup. Consequently, `library(zipcodeR)` does not load `sp`, `raster`,
+`terra`, `sf`, or emit their retirement message. A legacy distance call loads
+`raster` lazily and retains the exact 0.3.5 WGS84 result; the recommended `_ng`
+distance path uses the internal modern calculation and never invokes it.
+
+This removes the reported startup warning without rewriting historical
+scientific output.
diff --git a/issue_comments/issue-29.md b/issue_comments/issue-29.md
new file mode 100644
index 0000000..9f5eff5
--- /dev/null
+++ b/issue_comments/issue-29.md
@@ -0,0 +1,9 @@
+
+
+The default `zip_to_cd` object remains the exact 0.3.5 snapshot; replacing it
+changed and removed thousands of existing mappings. The modern pipeline now
+builds a separate 119th-Congress ZCTA relationship using only the authoritative
+Census file. It deliberately leaves non-derivable USPS-only ZIPs unmapped rather
+than assigning every district in the same city. The public checksum-verified
+`2026.08` bundle is available to 0.4.0 callers through
+`get_cd_ng(bundle, ...)`.
diff --git a/issue_comments/issue-32.md b/issue_comments/issue-32.md
new file mode 100644
index 0000000..81bfcc6
--- /dev/null
+++ b/issue_comments/issue-32.md
@@ -0,0 +1,18 @@
+
+
+You've diagnosed it exactly right: this is inherent to how ZIP codes work rather than
+something zipcodeR can fully fix. ZIP codes are postal delivery constructs — routes
+and delivery points, not polygons — and the "city" attached to a ZIP code is the
+USPS's preferred *mailing* name, which routinely extends beyond municipal limits
+(addresses outside a city's boundary often still carry that city's name).
+
+For accurate jurisdiction assignment you need the full street address geocoded against
+boundary files (e.g., Census TIGER/Line places), not the ZIP code alone. ZIP-code-level
+attributes in this package are estimated at the ZCTA level, which is itself an
+approximation.
+
+The 0.4.0 release documents these limitations honestly in `?zip_code_db` (new
+"Provenance and limitations" section) and in the FAQ vignette, so users hit this
+caveat before relying on the jurisdiction columns. Closing as addressed by
+documentation — happy to reopen if a concrete improvement to the data itself emerges
+(a place-based crosswalk is on the roadmap for consideration).
diff --git a/issue_comments/issue-33.md b/issue_comments/issue-33.md
new file mode 100644
index 0000000..b395544
--- /dev/null
+++ b/issue_comments/issue-33.md
@@ -0,0 +1,12 @@
+
+
+The per-row distance loop has been replaced by one vectorized
+`raster::pointDistance()` call, with a conservative bounding-box prefilter for
+ordinary valid scalar inputs. This retains the exact legacy WGS84 algorithm,
+membership, ordering, distances, and invalid-input conditions while reducing
+representative calls from about one second to roughly 10–15 ms locally.
+
+The benchmark asserts `identical()` results before reporting speed, and the
+isolated 0.3.5 compatibility harness covers radius boundaries and antimeridian
+cases. The legacy dependency remains because replacing the algorithm with
+haversine would change scientific results.
diff --git a/issue_comments/issue-7.md b/issue_comments/issue-7.md
new file mode 100644
index 0000000..06a0d22
--- /dev/null
+++ b/issue_comments/issue-7.md
@@ -0,0 +1,11 @@
+
+
+The recommended modern interface now includes FIPS identifiers in
+`reverse_zipcode_ng(bundle, ...)`. It returns a two-digit `state_fips` and the
+full five-digit `county_fips` for the predominant county represented by the
+ZIP-level record. For example, Alexandria city is represented by county FIPS
+`51510`.
+
+The unsuffixed `reverse_zipcode()` output remains exactly as it was in 0.3.5 so
+existing code that depends on its column schema is not silently changed. The
+new columns are therefore available through the explicit `_ng` contract.
diff --git a/issue_comments/pr-30.md b/issue_comments/pr-30.md
new file mode 100644
index 0000000..9a30421
--- /dev/null
+++ b/issue_comments/pr-30.md
@@ -0,0 +1,11 @@
+
+
+Thank you for establishing the Census CD-to-ZCTA relationship-file approach.
+The modernization pipeline follows that method for a separate, versioned
+119th-Congress bundle and credits this contribution.
+
+The default `zip_to_cd` object cannot be replaced in 0.4.0 because it would
+retcon existing research results. The new bundle also declines to infer
+districts for USPS-only ZIPs by city. The `2026.08` asset is now public and
+checksum-verified; 0.4.0 users can opt in through `get_cd_ng(bundle, ...)`,
+while the legacy object remains unchanged.
diff --git a/man/download_comprehensive_data.Rd b/man/download_comprehensive_data.Rd
new file mode 100644
index 0000000..2591012
--- /dev/null
+++ b/man/download_comprehensive_data.Rd
@@ -0,0 +1,36 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/data_version.R
+\name{download_comprehensive_data}
+\alias{download_comprehensive_data}
+\title{Download the comprehensive ZIP code database}
+\usage{
+download_comprehensive_data(force = FALSE)
+}
+\arguments{
+\item{force}{If TRUE, re-download even if a verified copy is cached.}
+}
+\value{
+Invisibly, the path to the downloaded SQLite database. Query it
+with DBI/RSQLite, e.g.
+\code{DBI::dbConnect(RSQLite::SQLite(), download_comprehensive_data())}.
+}
+\description{
+The bundled \code{zip_code_db} is the lightweight ("simple") dataset. A
+much larger companion database with detailed ACS demographic profiles per
+ZIP code (the "comprehensive" database, ~450 MB SQLite) is published as a
+checksum-pinned asset of a zipcodeR data release rather than shipped in the
+package. Its public URL, checksum, and SQLite integrity have passed a
+clean-machine smoke test.
+}
+\details{
+This function downloads that database, verifies its SHA256 checksum,
+and caches it under \code{tools::R_user_dir("zipcodeR", "data")}; later
+calls return the cached path immediately. It never downloads without being
+called explicitly. For offline use, copy the file to that directory
+yourself (the expected file name is the asset name from the data release).
+}
+\examples{
+\dontrun{
+path <- download_comprehensive_data()
+}
+}
diff --git a/man/download_zip_data.Rd b/man/download_zip_data.Rd
index 3c8c42d..e1c7b60 100644
--- a/man/download_zip_data.Rd
+++ b/man/download_zip_data.Rd
@@ -2,7 +2,7 @@
% Please edit documentation in R/download_data.r
\name{download_zip_data}
\alias{download_zip_data}
-\title{Download updated data files needed for library functionality to the package's data directory. To be implemented for future updates.}
+\title{Legacy in-place data downloader}
\usage{
download_zip_data(force = FALSE)
}
@@ -13,7 +13,13 @@ download_zip_data(force = FALSE)
Data files needed for package functionality, stored in data directory of package install
}
\description{
-Download updated data files needed for library functionality to the package's data directory. To be implemented for future updates.
+This function is retained without behavioral changes for reproducibility
+with zipcodeR 0.3.5. It attempts to modify files inside an installed package
+and resolves an upstream "latest" release, so it is unsuitable for a pinned
+research workflow. New work should use \code{\link[=download_zip_data_bundle]{download_zip_data_bundle()}} or
+\code{\link[=read_zip_data_bundle]{read_zip_data_bundle()}} and pass the resulting object explicitly to the
+\verb{_ng} API. No runtime warning is emitted because warnings are part of the
+frozen legacy contract.
}
\examples{
\dontrun{
diff --git a/man/download_zip_data_bundle.Rd b/man/download_zip_data_bundle.Rd
new file mode 100644
index 0000000..936bfe1
--- /dev/null
+++ b/man/download_zip_data_bundle.Rd
@@ -0,0 +1,21 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/data_bundle.R
+\name{download_zip_data_bundle}
+\alias{download_zip_data_bundle}
+\title{Download a versioned zipcodeR data bundle}
+\usage{
+download_zip_data_bundle(version, force = FALSE)
+}
+\arguments{
+\item{version}{Exact registered data version, for example \code{"2026.08"}.}
+
+\item{force}{Re-download and replace a verified cached copy.}
+}
+\value{
+A validated \code{zipcodeR_data_bundle}.
+}
+\description{
+Downloads one explicitly requested data release, verifies its checksum, and
+caches it in the user's data directory. The function never resolves aliases
+such as \code{"latest"}, and no lookup function downloads data implicitly.
+}
diff --git a/man/geocode_zip_ng.Rd b/man/geocode_zip_ng.Rd
new file mode 100644
index 0000000..800eef1
--- /dev/null
+++ b/man/geocode_zip_ng.Rd
@@ -0,0 +1,19 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/ng_functions.R
+\name{geocode_zip_ng}
+\alias{geocode_zip_ng}
+\title{Geocode ZIP codes using an explicit data bundle}
+\usage{
+geocode_zip_ng(data, zip_code)
+}
+\arguments{
+\item{data}{A \code{zipcodeR_data_bundle}.}
+
+\item{zip_code}{ZIP code(s).}
+}
+\value{
+One coordinate row per input, preserving order and duplicates.
+}
+\description{
+Geocode ZIP codes using an explicit data bundle
+}
diff --git a/man/get_cd_ng.Rd b/man/get_cd_ng.Rd
new file mode 100644
index 0000000..cda751b
--- /dev/null
+++ b/man/get_cd_ng.Rd
@@ -0,0 +1,19 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/ng_functions.R
+\name{get_cd_ng}
+\alias{get_cd_ng}
+\title{Get congressional districts using an explicit data bundle}
+\usage{
+get_cd_ng(data, zip_code)
+}
+\arguments{
+\item{data}{A \code{zipcodeR_data_bundle}.}
+
+\item{zip_code}{One five-digit ZIP code.}
+}
+\value{
+A named list with parallel \code{state_fips} and \code{district} vectors.
+}
+\description{
+Get congressional districts using an explicit data bundle
+}
diff --git a/man/get_tracts_ng.Rd b/man/get_tracts_ng.Rd
new file mode 100644
index 0000000..41a3f0a
--- /dev/null
+++ b/man/get_tracts_ng.Rd
@@ -0,0 +1,19 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/ng_functions.R
+\name{get_tracts_ng}
+\alias{get_tracts_ng}
+\title{Get Census tracts using an explicit data bundle}
+\usage{
+get_tracts_ng(data, zip_code)
+}
+\arguments{
+\item{data}{A \code{zipcodeR_data_bundle}.}
+
+\item{zip_code}{One five-digit ZIP or ZCTA code.}
+}
+\value{
+A tract crosswalk tibble.
+}
+\description{
+Get Census tracts using an explicit data bundle
+}
diff --git a/man/is_zcta_ng.Rd b/man/is_zcta_ng.Rd
new file mode 100644
index 0000000..fbf7469
--- /dev/null
+++ b/man/is_zcta_ng.Rd
@@ -0,0 +1,19 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/ng_functions.R
+\name{is_zcta_ng}
+\alias{is_zcta_ng}
+\title{Test ZCTA membership using an explicit data bundle}
+\usage{
+is_zcta_ng(data, zip_code)
+}
+\arguments{
+\item{data}{A \code{zipcodeR_data_bundle}.}
+
+\item{zip_code}{ZIP code(s).}
+}
+\value{
+A logical vector.
+}
+\description{
+Test ZCTA membership using an explicit data bundle
+}
diff --git a/man/normalize_zip_ng.Rd b/man/normalize_zip_ng.Rd
new file mode 100644
index 0000000..d286d35
--- /dev/null
+++ b/man/normalize_zip_ng.Rd
@@ -0,0 +1,17 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/ng_functions.R
+\name{normalize_zip_ng}
+\alias{normalize_zip_ng}
+\title{Normalize ZIP codes with corrected boundary behavior}
+\usage{
+normalize_zip_ng(zipcode)
+}
+\arguments{
+\item{zipcode}{Character or numeric ZIP values.}
+}
+\value{
+Normalized five-character ZIP values.
+}
+\description{
+Normalize ZIP codes with corrected boundary behavior
+}
diff --git a/man/read_zip_data_bundle.Rd b/man/read_zip_data_bundle.Rd
new file mode 100644
index 0000000..fadd72c
--- /dev/null
+++ b/man/read_zip_data_bundle.Rd
@@ -0,0 +1,23 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/data_bundle.R
+\name{read_zip_data_bundle}
+\alias{read_zip_data_bundle}
+\title{Read a versioned zipcodeR data bundle}
+\usage{
+read_zip_data_bundle(path, sha256 = NULL)
+}
+\arguments{
+\item{path}{Path to a bundle \code{.rds} file.}
+
+\item{sha256}{Optional expected SHA256 checksum. Supplying the checksum is
+strongly recommended when the file did not come from
+\code{\link[=download_zip_data_bundle]{download_zip_data_bundle()}}.}
+}
+\value{
+An object of class \code{zipcodeR_data_bundle}.
+}
+\description{
+Reads and validates a previously downloaded zipcodeR data bundle. Bundles
+are explicit, immutable inputs for the next-generation (\verb{_ng}) API; reading
+one never changes the datasets used by the legacy API.
+}
diff --git a/man/reverse_zipcode_ng.Rd b/man/reverse_zipcode_ng.Rd
new file mode 100644
index 0000000..ee963b1
--- /dev/null
+++ b/man/reverse_zipcode_ng.Rd
@@ -0,0 +1,21 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/ng_functions.R
+\name{reverse_zipcode_ng}
+\alias{reverse_zipcode_ng}
+\title{Reverse-geocode ZIP codes using an explicit data bundle}
+\usage{
+reverse_zipcode_ng(data, zip_code)
+}
+\arguments{
+\item{data}{A \code{zipcodeR_data_bundle}.}
+
+\item{zip_code}{Five-digit ZIP code(s).}
+}
+\value{
+One row per input, in input order, with duplicates preserved. The
+result adds \code{state_fips} (two digits) and \code{county_fips} (the full
+five-digit county identifier) for the database's predominant county.
+}
+\description{
+Reverse-geocode ZIP codes using an explicit data bundle
+}
diff --git a/man/search_cd_ng.Rd b/man/search_cd_ng.Rd
new file mode 100644
index 0000000..6268c54
--- /dev/null
+++ b/man/search_cd_ng.Rd
@@ -0,0 +1,21 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/ng_functions.R
+\name{search_cd_ng}
+\alias{search_cd_ng}
+\title{Search a congressional district using an explicit data bundle}
+\usage{
+search_cd_ng(data, state_fips_code, congressional_district)
+}
+\arguments{
+\item{data}{A \code{zipcodeR_data_bundle}.}
+
+\item{state_fips_code}{One state FIPS code.}
+
+\item{congressional_district}{One two-digit district code.}
+}
+\value{
+A ZIP-to-district tibble.
+}
+\description{
+Search a congressional district using an explicit data bundle
+}
diff --git a/man/search_city_ng.Rd b/man/search_city_ng.Rd
new file mode 100644
index 0000000..00ef8d4
--- /dev/null
+++ b/man/search_city_ng.Rd
@@ -0,0 +1,21 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/ng_functions.R
+\name{search_city_ng}
+\alias{search_city_ng}
+\title{Search a city using an explicit data bundle}
+\usage{
+search_city_ng(data, city_name, state_abb)
+}
+\arguments{
+\item{data}{A \code{zipcodeR_data_bundle}.}
+
+\item{city_name}{City name.}
+
+\item{state_abb}{Two-letter state abbreviation.}
+}
+\value{
+A tibble from the selected data vintage.
+}
+\description{
+Search a city using an explicit data bundle
+}
diff --git a/man/search_county_ng.Rd b/man/search_county_ng.Rd
new file mode 100644
index 0000000..9a15ca4
--- /dev/null
+++ b/man/search_county_ng.Rd
@@ -0,0 +1,23 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/ng_functions.R
+\name{search_county_ng}
+\alias{search_county_ng}
+\title{Search a county using an explicit data bundle}
+\usage{
+search_county_ng(data, county_name, state_abb, ...)
+}
+\arguments{
+\item{data}{A \code{zipcodeR_data_bundle}.}
+
+\item{county_name}{County name.}
+
+\item{state_abb}{Two-letter state abbreviation.}
+
+\item{...}{Set \code{similar = TRUE} and optionally \code{max.distance} for approximate matching.}
+}
+\value{
+A tibble from the selected data vintage.
+}
+\description{
+Search a county using an explicit data bundle
+}
diff --git a/man/search_fips_ng.Rd b/man/search_fips_ng.Rd
new file mode 100644
index 0000000..8d8fc1c
--- /dev/null
+++ b/man/search_fips_ng.Rd
@@ -0,0 +1,21 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/ng_functions.R
+\name{search_fips_ng}
+\alias{search_fips_ng}
+\title{Search FIPS codes using an explicit data bundle}
+\usage{
+search_fips_ng(data, state_fips, county_fips)
+}
+\arguments{
+\item{data}{A \code{zipcodeR_data_bundle}.}
+
+\item{state_fips}{One state FIPS code.}
+
+\item{county_fips}{Optional one- to three-digit county FIPS code.}
+}
+\value{
+A tibble from the selected data vintage.
+}
+\description{
+Search FIPS codes using an explicit data bundle
+}
diff --git a/man/search_radius_ng.Rd b/man/search_radius_ng.Rd
new file mode 100644
index 0000000..b5cf238
--- /dev/null
+++ b/man/search_radius_ng.Rd
@@ -0,0 +1,21 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/ng_functions.R
+\name{search_radius_ng}
+\alias{search_radius_ng}
+\title{Search within a radius using an explicit data bundle}
+\usage{
+search_radius_ng(data, lat, lng, radius = 1)
+}
+\arguments{
+\item{data}{A \code{zipcodeR_data_bundle}.}
+
+\item{lat, lng}{Query coordinates.}
+
+\item{radius}{Radius in miles.}
+}
+\value{
+A tibble of ZIP codes and haversine distances.
+}
+\description{
+Search within a radius using an explicit data bundle
+}
diff --git a/man/search_state_ng.Rd b/man/search_state_ng.Rd
new file mode 100644
index 0000000..8c615fd
--- /dev/null
+++ b/man/search_state_ng.Rd
@@ -0,0 +1,19 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/ng_functions.R
+\name{search_state_ng}
+\alias{search_state_ng}
+\title{Search a state using an explicit data bundle}
+\usage{
+search_state_ng(data, state_abb)
+}
+\arguments{
+\item{data}{A \code{zipcodeR_data_bundle}.}
+
+\item{state_abb}{Two-letter state abbreviation(s).}
+}
+\value{
+A tibble grouped in query order; repeated states repeat their rows.
+}
+\description{
+Search a state using an explicit data bundle
+}
diff --git a/man/search_tz_ng.Rd b/man/search_tz_ng.Rd
new file mode 100644
index 0000000..8d662ff
--- /dev/null
+++ b/man/search_tz_ng.Rd
@@ -0,0 +1,19 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/ng_functions.R
+\name{search_tz_ng}
+\alias{search_tz_ng}
+\title{Search a timezone using an explicit data bundle}
+\usage{
+search_tz_ng(data, tz)
+}
+\arguments{
+\item{data}{A \code{zipcodeR_data_bundle}.}
+
+\item{tz}{Timezone value(s).}
+}
+\value{
+A tibble grouped in query order; repeated timezones repeat their rows.
+}
+\description{
+Search a timezone using an explicit data bundle
+}
diff --git a/man/zcta_crosswalk.Rd b/man/zcta_crosswalk.Rd
index 878bd75..e3dc15c 100644
--- a/man/zcta_crosswalk.Rd
+++ b/man/zcta_crosswalk.Rd
@@ -5,7 +5,7 @@
\alias{zcta_crosswalk}
\title{ZCTA to Census Tract (2010) Crosswalk}
\format{
-A data frame with 148897 rows and 4 variables:
+A data frame with 148897 rows and 3 variables:
\describe{
\item{ZCTA5}{2010 ZIP Code Tabulation Area}
\item{TRACT}{2010 Census Tract Code}
@@ -21,4 +21,9 @@ zcta_crosswalk
\description{
A dataset containing the relationships between ZIP code tabulation areas (ZCTA) and Census Tracts. This contains selected variables from the official crosswalk file.
}
+\details{
+This is the exact dataset distributed by zipcodeR 0.3.5. It is intentionally
+frozen so legacy calls remain reproducible; newer versioned relationships
+are available only through an explicit data bundle and the \verb{_ng} API.
+}
\keyword{datasets}
diff --git a/man/zip_code_db.Rd b/man/zip_code_db.Rd
index 514b6bf..c34f4dd 100644
--- a/man/zip_code_db.Rd
+++ b/man/zip_code_db.Rd
@@ -8,7 +8,7 @@
A data frame with 41877 rows and 24 variables:
\describe{
\item{zipcode}{5 digit U.S. ZIP code}
-\item{zipcode_type}{2010 State FIPS Code}
+\item{zipcode_type}{ZIP category such as Standard, PO Box, or Unique}
\item{major_city}{Major city serving the ZIP code}
\item{post_office_city}{City of post office serving the ZIP code}
\item{common_city_list}{List of common cities represented by the ZIP code}
@@ -42,4 +42,10 @@ zip_code_db
\description{
A dataset containing detailed information for U.S. ZIP codes
}
+\details{
+This is the exact 41,877-row database distributed by zipcodeR 0.3.5,
+including its historical values and limitations. It is intentionally frozen
+for research reproducibility. Use an explicit data bundle with \verb{_ng}
+functions for corrected behavior and a pinned source manifest.
+}
\keyword{datasets}
diff --git a/man/zip_data_provenance.Rd b/man/zip_data_provenance.Rd
new file mode 100644
index 0000000..76ceffd
--- /dev/null
+++ b/man/zip_data_provenance.Rd
@@ -0,0 +1,23 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/data_bundle.R
+\name{zip_data_provenance}
+\alias{zip_data_provenance}
+\title{Inspect the provenance supplied with a zipcodeR data bundle}
+\usage{
+zip_data_provenance(bundle, dataset = NULL, key = NULL)
+}
+\arguments{
+\item{bundle}{A \code{zipcodeR_data_bundle}.}
+
+\item{dataset}{Optional dataset name (\code{"zip_code_db"},
+\code{"zcta_crosswalk"}, or \code{"zip_to_cd"}).}
+
+\item{key}{Optional record key (ZIP or ZCTA) to select.}
+}
+\value{
+A provenance data frame combining source/method records with
+record-level quality status and reasons.
+}
+\description{
+Inspect the provenance supplied with a zipcodeR data bundle
+}
diff --git a/man/zip_data_version.Rd b/man/zip_data_version.Rd
new file mode 100644
index 0000000..72003b5
--- /dev/null
+++ b/man/zip_data_version.Rd
@@ -0,0 +1,20 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/data_bundle.R
+\name{zip_data_version}
+\alias{zip_data_version}
+\title{Report zipcodeR data-version metadata}
+\usage{
+zip_data_version(x = NULL)
+}
+\arguments{
+\item{x}{\code{NULL} for the legacy bundled data, or a
+\code{zipcodeR_data_bundle} object.}
+}
+\value{
+A named metadata list.
+}
+\description{
+With no argument, reports the immutable data bundled with the legacy API.
+Given a downloaded data bundle, reports that bundle's metadata and verified
+checksum.
+}
diff --git a/man/zip_distance_ng.Rd b/man/zip_distance_ng.Rd
new file mode 100644
index 0000000..1932ef3
--- /dev/null
+++ b/man/zip_distance_ng.Rd
@@ -0,0 +1,30 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/ng_functions.R
+\name{zip_distance_ng}
+\alias{zip_distance_ng}
+\title{Calculate ZIP-to-ZIP distance using an explicit data bundle}
+\usage{
+zip_distance_ng(
+ data,
+ zipcode_a,
+ zipcode_b,
+ lonlat = TRUE,
+ units = c("miles", "meters")
+)
+}
+\arguments{
+\item{data}{A \code{zipcodeR_data_bundle}.}
+
+\item{zipcode_a, zipcode_b}{ZIP vectors.}
+
+\item{lonlat}{Use great-circle haversine distance; \code{FALSE} uses a planar
+equirectangular approximation.}
+
+\item{units}{\code{"miles"} or \code{"meters"}.}
+}
+\value{
+A data frame of paired ZIP codes and distances.
+}
+\description{
+Calculate ZIP-to-ZIP distance using an explicit data bundle
+}
diff --git a/man/zip_to_cd.Rd b/man/zip_to_cd.Rd
index b9ec058..156590c 100644
--- a/man/zip_to_cd.Rd
+++ b/man/zip_to_cd.Rd
@@ -20,4 +20,10 @@ zip_to_cd
\description{
A dataset containing mappings between ZIP codes and congressional districts
}
+\details{
+This is the exact pre-2020 HUD-USPS-derived mapping shipped in zipcodeR
+0.3.5. It is frozen for legacy reproducibility and should not be interpreted
+as a current redistricting vintage. Current authoritative ZCTA relationships
+are available in explicit data bundles.
+}
\keyword{datasets}
diff --git a/man/zipcodeR-package.Rd b/man/zipcodeR-package.Rd
new file mode 100644
index 0000000..06a938a
--- /dev/null
+++ b/man/zipcodeR-package.Rd
@@ -0,0 +1,47 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/zipcodeR-package.R
+\docType{package}
+\name{zipcodeR-package}
+\alias{zipcodeR-package}
+\alias{zipcodeR}
+\title{zipcodeR: reproducible U.S. ZIP-code analysis}
+\description{
+zipcodeR provides two deliberately separate interfaces.
+}
+\section{Recommended interface for new analyses}{
+
+Use functions ending in \verb{_ng} with an explicitly selected data bundle. Start
+with \code{\link[=download_zip_data_bundle]{download_zip_data_bundle()}} for a registered immutable version or
+\code{\link[=read_zip_data_bundle]{read_zip_data_bundle()}} for a local checksum-pinned file. Passing the bundle
+as the first argument makes the chosen data vintage visible in every call.
+\verb{_ng} never resolves an implicit \code{latest} version or downloads during a
+lookup.
+}
+
+\section{Historical compatibility interface}{
+
+Unsuffixed functions and the datasets in \verb{data/} retain the exact zipcodeR
+0.3.5 contract for existing scripts and research reproduction. This includes
+historical data, scientific algorithms, conditions, ordering, and known edge
+cases. Use \code{\link[=zip_data_version]{zip_data_version()}} to record which contract an analysis used.
+}
+
+\seealso{
+Useful links:
+\itemize{
+ \item \url{https://github.com/gavinrozzi/zipcodeR/}
+ \item \url{https://www.gavinrozzi.com/project/zipcoder/}
+ \item Report bugs at \url{https://github.com/gavinrozzi/zipcodeR/issues/}
+}
+
+}
+\author{
+\strong{Maintainer}: Gavin Rozzi \email{gr@gavinrozzi.com} (\href{https://orcid.org/0000-0002-9969-8175}{ORCID})
+
+Authors:
+\itemize{
+ \item Gavin Rozzi \email{gr@gavinrozzi.com} (\href{https://orcid.org/0000-0002-9969-8175}{ORCID})
+}
+
+}
+\keyword{internal}
diff --git a/tests/testthat/helper-bundle.R b/tests/testthat/helper-bundle.R
new file mode 100644
index 0000000..3102145
--- /dev/null
+++ b/tests/testthat/helper-bundle.R
@@ -0,0 +1,71 @@
+make_test_bundle <- function() {
+ ns <- asNamespace("zipcodeR")
+ legacy_db <- get("zip_code_db", ns)
+ wanted <- c("08731", "08901", "08734", "08005", "90210")
+ db <- legacy_db[match(wanted, legacy_db$zipcode), , drop = FALSE]
+
+ synthetic <- db[rep(1L, 4L), , drop = FALSE]
+ synthetic$zipcode <- c("99001", "99002", "99003", "99004")
+ synthetic$state <- c("AK", "AK", "AK", "AK")
+ synthetic$county <- c("Test County", "Test County", "Test County", "Test County")
+ synthetic$major_city <- c("East Edge", "West Edge", "Far Point", "No Point")
+ synthetic$lat <- c(0, 0, 0, NA_real_)
+ synthetic$lng <- c(179.9, -179.9, 0, NA_real_)
+ db <- rbind(db, synthetic)
+ rownames(db) <- NULL
+
+ zcta <- data.frame(
+ ZCTA5 = c("08731", "08731", "90210", "99001", "99002"),
+ TRACT = c("010100", "010200", "700100", "000100", "000200"),
+ GEOID = c(
+ "34029010100", "34029010200", "06037700100",
+ "02013000100", "02013000200"
+ ),
+ stringsAsFactors = FALSE
+ )
+ cd <- data.frame(
+ ZIP = c("08731", "08731", "90210", "99001"),
+ CD = c("3402", "3404", "0636", "0200"),
+ stringsAsFactors = FALSE
+ )
+ provenance <- data.frame(
+ dataset = c("zip_code_db", "zip_to_cd"),
+ key = c("08731", "08731"),
+ field = c("coordinates", "record"),
+ source_id = c("test_gazetteer", "test_cd"),
+ method = c("test fixture", "authoritative relationship"),
+ quality = c("authoritative_source", "authoritative_source"),
+ note = c("", ""),
+ stringsAsFactors = FALSE
+ )
+ quality <- data.frame(
+ dataset = "zip_to_cd",
+ key = "99004",
+ field = "CD",
+ status = "unmapped",
+ reason = "No authoritative relationship in test fixture",
+ stringsAsFactors = FALSE
+ )
+ structure(
+ list(
+ zip_code_db = db,
+ zcta_crosswalk = zcta,
+ zip_to_cd = cd,
+ metadata = list(
+ data_version = "test-2026.08",
+ build_timestamp = "2026-08-24T00:00:00Z",
+ output_hashes = list(
+ zip_code_db = paste(rep("1", 64), collapse = ""),
+ zcta_crosswalk = paste(rep("2", 64), collapse = ""),
+ zip_to_cd = paste(rep("3", 64), collapse = ""),
+ provenance = paste(rep("4", 64), collapse = ""),
+ quality = paste(rep("5", 64), collapse = "")
+ )
+ ),
+ provenance = provenance,
+ quality = quality
+ ),
+ class = c("zipcodeR_data_bundle", "list"),
+ bundle_sha256 = paste(rep("a", 64), collapse = "")
+ )
+}
diff --git a/tests/testthat/test-00-load-contract.R b/tests/testthat/test-00-load-contract.R
new file mode 100644
index 0000000..1777ba4
--- /dev/null
+++ b/tests/testthat/test-00-load-contract.R
@@ -0,0 +1,5 @@
+test_that("legacy spatial namespaces are not eagerly imported", {
+ namespace <- readLines(system.file("NAMESPACE", package = "zipcodeR"))
+ expect_false(any(grepl("^import\\(tidycensus\\)$", namespace)))
+ expect_false(any(grepl("^importFrom\\(raster,", namespace)))
+})
diff --git a/tests/testthat/test-04-data-bundles.R b/tests/testthat/test-04-data-bundles.R
new file mode 100644
index 0000000..63fca7d
--- /dev/null
+++ b/tests/testthat/test-04-data-bundles.R
@@ -0,0 +1,206 @@
+test_that("bundle files are schema- and checksum-validated", {
+ bundle <- make_test_bundle()
+ path <- tempfile(fileext = ".rds")
+ saveRDS(bundle, path, version = 3)
+ sha <- zipcodeR:::file_sha256(path)
+
+ loaded <- read_zip_data_bundle(path, sha256 = sha)
+ expect_s3_class(loaded, "zipcodeR_data_bundle")
+ expect_identical(attr(loaded, "bundle_sha256"), sha)
+ expect_identical(zip_data_version(loaded)$data_version, "test-2026.08")
+
+ con <- file(path, open = "ab")
+ writeBin(as.raw(0), con)
+ close(con)
+ expect_error(read_zip_data_bundle(path, sha256 = sha), "Checksum verification failed")
+})
+
+test_that("malformed bundles and version aliases are rejected", {
+ bundle <- make_test_bundle()
+ bundle$quality <- data.frame(nope = "x")
+ expect_error(zipcodeR:::validate_zip_data_bundle(bundle), "quality")
+
+ bundle <- make_test_bundle()
+ bundle$zcta_crosswalk$GEOID <- as.numeric(bundle$zcta_crosswalk$GEOID)
+ expect_error(zipcodeR:::validate_zip_data_bundle(bundle), "types")
+
+ expect_error(download_zip_data_bundle("latest"), "Aliases")
+ expect_error(download_zip_data_bundle("not-registered"), "Unknown")
+})
+
+test_that("release manifest tampering is rejected", {
+ bundle <- make_test_bundle()
+ asset <- tempfile(pattern = "zipcodeR-data-", fileext = ".rds")
+ saveRDS(bundle, asset, version = 3)
+ manifest <- list(
+ data_version = bundle$metadata$data_version,
+ asset = basename(asset),
+ asset_sha256 = zipcodeR:::file_sha256(asset),
+ rows = list(
+ zip_code_db = nrow(bundle$zip_code_db),
+ zcta_crosswalk = nrow(bundle$zcta_crosswalk),
+ zip_to_cd = nrow(bundle$zip_to_cd)
+ ),
+ schemas = list(
+ zip_code_db = lapply(names(bundle$zip_code_db), function(name) {
+ list(name = name, type = class(bundle$zip_code_db[[name]])[1])
+ }),
+ zcta_crosswalk = lapply(names(bundle$zcta_crosswalk), function(name) {
+ list(name = name, type = class(bundle$zcta_crosswalk[[name]])[1])
+ }),
+ zip_to_cd = lapply(names(bundle$zip_to_cd), function(name) {
+ list(name = name, type = class(bundle$zip_to_cd[[name]])[1])
+ })
+ ),
+ sources = list(list(id = "fixture", sha256 = paste(rep("6", 64), collapse = ""))),
+ output_hashes = bundle$metadata$output_hashes,
+ pipeline_commit = "fixture",
+ r_version = R.version.string,
+ dependency_lock_sha256 = paste(rep("7", 64), collapse = "")
+ )
+ manifest_path <- tempfile(fileext = ".json")
+ jsonlite::write_json(manifest, manifest_path, auto_unbox = TRUE)
+ expect_s3_class(
+ zipcodeR:::validate_zip_data_release_manifest(manifest_path, asset),
+ "zipcodeR_data_bundle"
+ )
+
+ pinned_manifest_sha <- zipcodeR:::file_sha256(manifest_path)
+ manifest$rows$zip_code_db <- manifest$rows$zip_code_db + 1L
+ jsonlite::write_json(manifest, manifest_path, auto_unbox = TRUE)
+ expect_error(
+ zipcodeR:::validate_zip_data_release_manifest(
+ manifest_path, asset, manifest_sha256 = pinned_manifest_sha
+ ),
+ "Manifest checksum"
+ )
+ expect_error(
+ zipcodeR:::validate_zip_data_release_manifest(manifest_path, asset),
+ "row counts"
+ )
+})
+
+test_that("provenance can be selected by dataset and key", {
+ bundle <- make_test_bundle()
+ result <- zip_data_provenance(bundle, "zip_to_cd", "08731")
+ expect_equal(nrow(result), 1L)
+ expect_identical(result$source_id, "test_cd")
+ expect_error(zip_data_provenance(bundle, "made_up"), "Unknown")
+})
+
+test_that("verified cache works offline and corrupt cache is replaced atomically", {
+ bundle <- make_test_bundle()
+ source <- tempfile(fileext = ".rds")
+ saveRDS(bundle, source, version = 3)
+ sha <- zipcodeR:::file_sha256(source)
+ cache <- tempfile("bundle-cache-")
+ dir.create(cache)
+ meta <- list(
+ version = "test-2026.08", asset = "bundle.rds",
+ url = "test://bundle", sha256 = sha
+ )
+ file.copy(source, file.path(cache, meta$asset))
+
+ testthat::local_mocked_bindings(
+ zipcodeR_has_internet = function() stop("network should not be consulted"),
+ .package = "zipcodeR"
+ )
+ expect_s3_class(
+ zipcodeR:::download_zip_data_bundle_from(meta, FALSE, cache),
+ "zipcodeR_data_bundle"
+ )
+})
+
+test_that("offline, corrupt, and interrupted downloads cannot become cache hits", {
+ bundle <- make_test_bundle()
+ source <- tempfile(fileext = ".rds")
+ saveRDS(bundle, source, version = 3)
+ sha <- zipcodeR:::file_sha256(source)
+ meta <- list(
+ version = "test-2026.08", asset = "bundle.rds",
+ url = "test://bundle", sha256 = sha
+ )
+
+ offline_cache <- tempfile("offline-cache-")
+ dir.create(offline_cache)
+ testthat::local_mocked_bindings(
+ zipcodeR_has_internet = function() FALSE,
+ .package = "zipcodeR"
+ )
+ expect_error(
+ zipcodeR:::download_zip_data_bundle_from(meta, FALSE, offline_cache),
+ "No internet connection"
+ )
+ expect_false(file.exists(file.path(offline_cache, meta$asset)))
+
+ testthat::local_mocked_bindings(
+ zipcodeR_has_internet = function() TRUE,
+ download_zip_data_file = function(url, path) {
+ writeBin(as.raw(c(1, 2, 3)), path)
+ stop("simulated interruption")
+ },
+ .package = "zipcodeR"
+ )
+ expect_error(
+ zipcodeR:::download_zip_data_bundle_from(meta, FALSE, offline_cache),
+ "simulated interruption"
+ )
+ expect_false(file.exists(file.path(offline_cache, meta$asset)))
+
+ writeBin(as.raw(c(9, 9, 9)), file.path(offline_cache, meta$asset))
+ testthat::local_mocked_bindings(
+ zipcodeR_has_internet = function() TRUE,
+ download_zip_data_file = function(url, path) file.copy(source, path),
+ .package = "zipcodeR"
+ )
+ expect_message(
+ loaded <- zipcodeR:::download_zip_data_bundle_from(meta, FALSE, offline_cache),
+ "failed checksum"
+ )
+ expect_s3_class(loaded, "zipcodeR_data_bundle")
+ expect_identical(zipcodeR:::file_sha256(file.path(offline_cache, meta$asset)), sha)
+})
+
+test_that("Windows SHA selection excludes batch wrappers", {
+ bins <- c(sha256sum = "C:/tools/sha256sum.exe", shasum = "C:/Strawberry/shasum.bat")
+ expect_identical(
+ zipcodeR:::sha256_system_binary("windows", bins),
+ "C:/tools/sha256sum.exe"
+ )
+ expect_length(
+ zipcodeR:::sha256_system_binary("windows", c(shasum = "C:/Strawberry/shasum.bat")),
+ 0L
+ )
+})
+
+test_that("published registries pin the independently verified assets", {
+ bundle_meta <- zipcodeR:::registered_zip_data_bundle("2026.08")
+ expect_identical(
+ bundle_meta$sha256,
+ "9059026c159a4d1311ad9c61ba5193a6123299503efb5d867b8303c9d23627e4"
+ )
+ expect_match(bundle_meta$url, "/data-2026.08/zipcodeR-data-2026.08\\.rds$")
+
+ comprehensive_meta <- zipcodeR:::comprehensive_data_registry()
+ expect_true(comprehensive_meta$published)
+ expect_identical(
+ comprehensive_meta$sha256,
+ "d85ed4e25884bc27bdd339d57dd9e2d1763531d4c050acb7a05a3d5aca90668d"
+ )
+
+ cache <- tempfile("comprehensive-cache-")
+ dir.create(cache)
+ cached_file <- file.path(cache, comprehensive_meta$asset)
+ writeBin(as.raw(1), cached_file)
+ testthat::local_mocked_bindings(
+ zipcodeR_user_data_dir = function() cache,
+ file_sha256 = function(path) comprehensive_meta$sha256,
+ ensure_sha256_available = function() invisible(TRUE),
+ .package = "zipcodeR"
+ )
+ expect_message(
+ result <- download_comprehensive_data(),
+ "using cached comprehensive database"
+ )
+ expect_identical(result, cached_file)
+})
diff --git a/tests/testthat/test-05-ng-api.R b/tests/testthat/test-05-ng-api.R
new file mode 100644
index 0000000..4fa60db
--- /dev/null
+++ b/tests/testthat/test-05-ng-api.R
@@ -0,0 +1,116 @@
+test_that("next-generation reverse lookup preserves order, duplicates, and misses", {
+ bundle <- make_test_bundle()
+ expect_warning(
+ result <- reverse_zipcode_ng(bundle, c("08731", "99999", "08731")),
+ "99999"
+ )
+ expect_identical(result$zipcode, c("08731", "99999", "08731"))
+ expect_identical(result$state_fips, c("34", NA_character_, "34"))
+ expect_identical(result$county_fips, c("34029", NA_character_, "34029"))
+ expect_true(is.na(result$state[[2]]))
+ expect_identical(attr(result, "zipcodeR_data_version"), "test-2026.08")
+ expect_identical(attr(result, "zipcodeR_bundle_sha256"), paste(rep("a", 64), collapse = ""))
+})
+
+test_that("next-generation geocoding preserves one row per input", {
+ bundle <- make_test_bundle()
+ expect_warning(
+ result <- geocode_zip_ng(bundle, c("08731", "99999", "08731")),
+ "99999"
+ )
+ expect_identical(result$zipcode, c("08731", "99999", "08731"))
+ expect_true(is.na(result$lat[[2]]))
+ expect_error(geocode_zip_ng(bundle, "99999"), "No results")
+ expect_equal(nrow(geocode_zip_ng(bundle, character())), 0L)
+})
+
+test_that("next-generation radius search handles antimeridian and boundaries", {
+ bundle <- make_test_bundle()
+ result <- search_radius_ng(bundle, 0, 180, radius = 20)
+ expect_setequal(result$zipcode, c("99001", "99002"))
+
+ wide <- search_radius_ng(bundle, 0, 180, radius = 20)
+ threshold <- wide$distance[wide$zipcode == "99001"]
+ expect_true("99001" %in% search_radius_ng(bundle, 0, 180, threshold)$zipcode)
+ expect_false("99001" %in% suppressWarnings(
+ search_radius_ng(bundle, 0, 180, threshold - 1e-8)
+ )$zipcode)
+ expect_error(search_radius_ng(bundle, 91, 0, 1), "lat")
+ expect_error(search_radius_ng(bundle, 0, 181, 1), "lng")
+ expect_error(search_radius_ng(bundle, 0, 0, -1), "radius")
+ expect_error(search_radius_ng(bundle, 0, 0, Inf), "radius")
+})
+
+test_that("unavailable coordinates remain explicit", {
+ bundle <- make_test_bundle()
+ distance <- zip_distance_ng(bundle, "99004", "08731")
+ expect_true(is.na(distance$distance))
+ radius <- search_radius_ng(bundle, 0, 180, 200)
+ expect_false("99004" %in% radius$zipcode)
+})
+
+test_that("next-generation tract and district results use authoritative bundle rows", {
+ bundle <- make_test_bundle()
+ tracts <- get_tracts_ng(bundle, "08731")
+ expect_identical(tracts$GEOID, c("34029010100", "34029010200"))
+
+ cd <- get_cd_ng(bundle, "08731")
+ expect_identical(cd$state_fips, c("NJ", "NJ"))
+ expect_identical(cd$district, c("02", "04"))
+
+ expect_warning(unmapped <- get_cd_ng(bundle, "99004"), "No authoritative")
+ expect_length(unmapped$district, 0L)
+ expect_error(get_cd_ng(bundle, "1234"), "5 digits")
+})
+
+test_that("next-generation distance recycling and normalization are explicit", {
+ bundle <- make_test_bundle()
+ result <- zip_distance_ng(
+ bundle,
+ c("08731", "08734"),
+ c("08901", "08005", "90210", "99004")
+ )
+ expect_equal(nrow(result), 4L)
+ expect_error(
+ zip_distance_ng(bundle, c("08731", "08734"), c("08901", "08005", "90210")),
+ "incompatible"
+ )
+ expect_error(zip_distance_ng(bundle, "08731", "08901", units = "furlongs"))
+ expect_error(zip_distance_ng(bundle, "08731", "08901", lonlat = NA), "lonlat")
+ expect_identical(normalize_zip_ng(100000), "00010")
+ expect_identical(normalize_zip_ng(c("99999-9999", NA)), c("99999", NA_character_))
+})
+
+test_that("next-generation searches use only the supplied bundle", {
+ bundle <- make_test_bundle()
+ expect_setequal(search_state_ng(bundle, "ak")$zipcode, c("99001", "99002", "99003", "99004"))
+ expect_identical(search_city_ng(bundle, "east Edge", "ak")$zipcode, "99001")
+ expect_true(all(search_fips_ng(bundle, "2")$state == "AK"))
+ expect_error(search_fips_ng(bundle, "XX"), "digits")
+ expect_error(search_fips_ng(bundle, "02", "not-a-code"), "digits")
+ expect_identical(search_cd_ng(bundle, "34", "2")$ZIP, "08731")
+ expect_error(search_cd_ng(bundle, "NJ", "2"), "digits")
+})
+
+test_that("vector searches preserve query order and duplicates", {
+ bundle <- make_test_bundle()
+ states <- search_state_ng(bundle, c("NJ", "AK", "NJ"))
+ nj_count <- sum(bundle$zip_code_db$state == "NJ")
+ ak_count <- sum(bundle$zip_code_db$state == "AK")
+ expect_identical(
+ states$state,
+ c(rep("NJ", nj_count), rep("AK", ak_count), rep("NJ", nj_count))
+ )
+
+ zones <- bundle$zip_code_db$timezone[
+ match(c("08731", "90210"), bundle$zip_code_db$zipcode)
+ ]
+ tz_result <- search_tz_ng(bundle, zones[c(2, 1, 2)])
+ expected <- unlist(lapply(zones[c(2, 1, 2)], function(zone) {
+ bundle$zip_code_db$zipcode[bundle$zip_code_db$timezone == zone]
+ }), use.names = FALSE)
+ expect_identical(tz_result$zipcode, expected)
+ expect_warning(search_state_ng(bundle, c("NJ", "ZZ")), "ZZ")
+ expect_error(search_state_ng(bundle, character()), "one or more")
+ expect_error(search_county_ng(bundle, "Ocean", "NJ", surprise = TRUE), "Unknown")
+})
diff --git a/tools/compatibility-check.R b/tools/compatibility-check.R
new file mode 100644
index 0000000..3c2f71f
--- /dev/null
+++ b/tools/compatibility-check.R
@@ -0,0 +1,199 @@
+#!/usr/bin/env Rscript
+
+# Install the frozen baseline and the candidate into isolated libraries, run
+# the same calls in separate R processes, and require byte-level R-object
+# identity. This intentionally compares conditions as well as return values.
+
+args <- commandArgs(trailingOnly = FALSE)
+file_arg <- grep("^--file=", args, value = TRUE)
+repo <- if (length(file_arg)) {
+ normalizePath(file.path(dirname(sub("^--file=", "", file_arg[[1]])), ".."))
+} else {
+ normalizePath(".")
+}
+cli_args <- commandArgs(trailingOnly = TRUE)
+baseline_arg <- grep("^--baseline-ref=", cli_args, value = TRUE)
+baseline_ref <- if (length(baseline_arg)) {
+ sub("^--baseline-ref=", "", baseline_arg[[1]])
+} else {
+ Sys.getenv("ZIPCODER_BASELINE_REF", "master")
+}
+if (!nzchar(baseline_ref)) stop("The baseline git ref must not be empty.")
+keep <- identical(Sys.getenv("KEEP_COMPAT_ARTIFACTS"), "true")
+root <- tempfile("zipcodeR-compat-")
+dir.create(root)
+if (!keep) on.exit(unlink(root, recursive = TRUE, force = TRUE), add = TRUE)
+
+base_src <- file.path(root, "baseline-source")
+base_lib <- file.path(root, "baseline-library")
+candidate_lib <- file.path(root, "candidate-library")
+dir.create(base_src)
+dir.create(base_lib)
+dir.create(candidate_lib)
+
+archive <- file.path(root, "baseline.tar")
+archive_log <- file.path(root, "baseline-archive.log")
+status <- system2(
+ "git", c("-C", shQuote(repo), "archive", "--format=tar", baseline_ref),
+ stdout = archive, stderr = archive_log
+)
+if (!identical(status, 0L)) {
+ cat(readLines(archive_log, warn = FALSE), sep = "\n")
+ stop("Could not archive baseline git ref '", baseline_ref, "'.")
+}
+utils::untar(archive, exdir = base_src)
+
+install_one <- function(source, library, log) {
+ r_bin <- file.path(R.home("bin"), "R")
+ status <- system2(
+ r_bin,
+ c("CMD", "INSTALL", "--no-multiarch", "--with-keep.source",
+ "-l", shQuote(library), shQuote(source)),
+ stdout = log, stderr = log
+ )
+ if (!identical(status, 0L)) {
+ cat(readLines(log, warn = FALSE), sep = "\n")
+ stop("Package installation failed; see ", log)
+ }
+}
+
+install_one(base_src, base_lib, file.path(root, "baseline-install.log"))
+install_one(repo, candidate_lib, file.path(root, "candidate-install.log"))
+
+worker <- file.path(root, "worker.R")
+writeLines(c(
+ "args <- commandArgs(trailingOnly = TRUE)",
+ "lib <- args[[1]]; output <- args[[2]]",
+ "suppressPackageStartupMessages(library(zipcodeR, lib.loc = lib))",
+ "capture_call <- function(expr) {",
+ " conditions <- list()",
+ " add_condition <- function(kind, condition) {",
+ " conditions[[length(conditions) + 1L]] <<- list(",
+ " kind = kind, class = class(condition),",
+ " message = conditionMessage(condition),",
+ " call = paste(deparse(conditionCall(condition)), collapse = ' ')",
+ " )",
+ " }",
+ " result <- withCallingHandlers(",
+ " tryCatch(",
+ " list(ok = TRUE, value = eval(expr, envir = .GlobalEnv)),",
+ " error = function(e) { add_condition('error', e); list(ok = FALSE) }",
+ " ),",
+ " warning = function(w) { add_condition('warning', w); invokeRestart('muffleWarning') },",
+ " message = function(m) { add_condition('message', m); invokeRestart('muffleMessage') }",
+ " )",
+ " list(result = result, conditions = conditions)",
+ "}",
+ "cases <- list(",
+ " state_one = quote(search_state('NJ')),",
+ " state_vector = quote(search_state(c('NJ', 'NY', 'CT'))),",
+ " state_invalid = quote(search_state('XY')),",
+ " county = quote(search_county('Ocean', 'NJ')),",
+ " county_similar = quote(search_county('ST BERNARD', 'LA', similar = TRUE, max.distance = 0.5)),",
+ " county_invalid = quote(search_county('Kenosha', 'NJ')),",
+ " city = quote(search_city('wayne', 'nj')),",
+ " city_invalid = quote(search_city('anytown', 'NJ')),",
+ " timezone = quote(search_tz('Mountain')),",
+ " timezone_invalid = quote(search_tz('Western')),",
+ " fips_state = quote(search_fips('34')),",
+ " fips_county = quote(search_fips('34', '3')),",
+ " fips_invalid = quote(search_fips('99')),",
+ " tracts = quote(get_tracts('08731')),",
+ " tracts_invalid = quote(get_tracts('999999')),",
+ " cd = quote(get_cd('08731')),",
+ " cd_cross_state = quote(get_cd('02861')),",
+ " cd_missing = quote(get_cd('99999')),",
+ " search_cd = quote(search_cd('34', '03')),",
+ " zcta_vector = quote(is_zcta(c('07762', '08999', NA))),",
+ " reverse_one = quote(reverse_zipcode('08731')),",
+ " reverse_duplicates = quote(reverse_zipcode(c('08731', '08999', '08731'))),",
+ " reverse_bad_width = quote(reverse_zipcode('099999')),",
+ " reverse_na = quote(reverse_zipcode(NA)),",
+ " reverse_empty = quote(reverse_zipcode()),",
+ " geocode_one = quote(geocode_zip('08731')),",
+ " geocode_vector = quote(geocode_zip(c('08731', '08721', '08731'))),",
+ " geocode_missing = quote(geocode_zip(c('08731', '99999'))),",
+ " geocode_all_missing = quote(geocode_zip('99999')),",
+ " radius_nj = quote(search_radius(39.9, -74.3, 10)),",
+ " radius_nyc = quote(search_radius(40.7128, -74.0060, 25)),",
+ " radius_la = quote(search_radius(34.0522, -118.2437, 50)),",
+ " radius_antimeridian = quote(search_radius(52, 179.5, 500)),",
+ " radius_zero = quote(search_radius(39.9, -74.3, 0)),",
+ " radius_empty = quote(search_radius(0, 0, 1)),",
+ " radius_negative = quote(search_radius(39.9, -74.3, -1)),",
+ " normalize_character = quote(normalize_zip(c('1', '99999-9999', NA))),",
+ " normalize_numeric = quote(normalize_zip(c(1, 100000, NA))),",
+ " normalize_null = quote(normalize_zip(NULL)),",
+ " distance = quote(zip_distance('08731', '08901')),",
+ " distance_vector = quote(zip_distance(c('08731', '08734'), c('08901', '08005'))),",
+ " distance_planar = quote(zip_distance('08731', '08901', lonlat = FALSE)),",
+ " distance_invalid_units = quote(zip_distance('08731', '08901', units = 'furlongs'))",
+ ")",
+ "ns <- asNamespace('zipcodeR')",
+ "exports <- getNamespaceExports('zipcodeR')",
+ "functions <- exports[vapply(exports, function(x) is.function(get(x, ns)), logical(1))]",
+ "functions <- stats::setNames(functions, functions)",
+ "out <- list(",
+ " datasets = list(",
+ " zip_code_db = get('zip_code_db', ns),",
+ " zcta_crosswalk = get('zcta_crosswalk', ns),",
+ " zip_to_cd = get('zip_to_cd', ns)",
+ " ),",
+ " cases = lapply(cases, capture_call),",
+ " formals = lapply(functions, function(x) formals(get(x, ns))),",
+ " download_zip_data_body = deparse(body(get('download_zip_data', ns)))",
+ ")",
+ "saveRDS(out, output, version = 2)"
+), worker)
+
+run_worker <- function(library, output, log) {
+ rscript <- file.path(R.home("bin"), "Rscript")
+ status <- system2(
+ rscript, c(shQuote(worker), shQuote(library), shQuote(output)),
+ stdout = log, stderr = log
+ )
+ if (!identical(status, 0L)) {
+ cat(readLines(log, warn = FALSE), sep = "\n")
+ stop("Compatibility worker failed; see ", log)
+ }
+}
+
+base_output <- file.path(root, "baseline.rds")
+candidate_output <- file.path(root, "candidate.rds")
+run_worker(base_lib, base_output, file.path(root, "baseline-worker.log"))
+run_worker(candidate_lib, candidate_output, file.path(root, "candidate-worker.log"))
+baseline <- readRDS(base_output)
+candidate <- readRDS(candidate_output)
+
+failures <- character()
+check_identical <- function(label, x, y) {
+ if (!identical(x, y)) {
+ failures <<- c(failures, label)
+ cat("FAIL: ", label, "\n", sep = "")
+ print(all.equal(x, y, tolerance = 0, check.attributes = TRUE))
+ } else {
+ cat("PASS: ", label, "\n", sep = "")
+ }
+}
+
+for (name in names(baseline$datasets)) {
+ check_identical(paste0("dataset ", name), baseline$datasets[[name]], candidate$datasets[[name]])
+}
+for (name in names(baseline$cases)) {
+ check_identical(paste0("call ", name), baseline$cases[[name]], candidate$cases[[name]])
+}
+for (name in names(baseline$formals)) {
+ check_identical(paste0("formals ", name), baseline$formals[[name]], candidate$formals[[name]])
+}
+check_identical(
+ "download_zip_data implementation",
+ baseline$download_zip_data_body,
+ candidate$download_zip_data_body
+)
+
+if (length(failures)) {
+ if (keep) cat("Artifacts retained at: ", root, "\n", sep = "")
+ stop("Legacy compatibility failed for ", length(failures), " comparison(s).")
+}
+cat("Legacy compatibility passed: all datasets, calls, conditions, and formals are identical.\n")
+if (keep) cat("Artifacts retained at: ", root, "\n", sep = "")
diff --git a/vignettes/data-vintages.Rmd b/vignettes/data-vintages.Rmd
new file mode 100644
index 0000000..f016292
--- /dev/null
+++ b/vignettes/data-vintages.Rmd
@@ -0,0 +1,105 @@
+---
+title: "Legacy and next-generation data"
+output: rmarkdown::html_vignette
+vignette: >
+ %\VignetteIndexEntry{Legacy and next-generation data}
+ %\VignetteEngine{knitr::rmarkdown}
+ %\VignetteEncoding{UTF-8}
+---
+
+```{r, include = FALSE}
+knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
+library(zipcodeR)
+```
+
+## Which interface should I use?
+
+ZIP and spatial data change over time, but silently replacing package data can
+change already-published analyses. zipcodeR therefore separates the historical
+contract from corrected, versioned behavior.
+
+For a new analysis, use the `_ng` functions with an explicitly selected modern
+bundle. This is the forward-looking interface documented for new community
+code. Calling an `_ng` function communicates that the analysis opted into the
+bundle's newer data and semantics; it does **not** opt into automatic future
+updates. Keep the version and SHA pinned for the life of the project.
+
+Use the unsuffixed functions when maintaining, rerunning, or comparing existing
+zipcodeR code. They are the historical compatibility interface.
+
+All names that existed in 0.3.5 keep the same data, algorithm, ordering,
+rounding, warnings, errors, and unusual edge cases. This is intentional even
+where a behavior would be designed differently today.
+
+```{r}
+zip_distance("08731", "08901")
+get_cd("08731")
+reverse_zipcode(c("08731", "08999", "08731"))
+zip_data_version()
+```
+
+The returned metadata identifies the legacy snapshot. No option or network
+state can switch these functions to newer data.
+
+## Start a new analysis by pinning a modern bundle
+
+A data release contains one RDS bundle and a JSON manifest. The manifest lists
+the bundle SHA256, every raw source URL and SHA256, licenses, vintages, build
+commit and R version, dependency-lock checksum, schemas, row counts, and
+canonical output hashes.
+
+After a version has been published and registered, download it explicitly:
+
+```{r, eval = FALSE}
+bundle <- download_zip_data_bundle("2026.08")
+```
+
+For offline or archival work, keep the RDS file with the project and verify the
+manifest checksum while reading it:
+
+```{r, eval = FALSE}
+bundle <- read_zip_data_bundle(
+ "data/zipcodeR-data-2026.08.rds",
+ sha256 = "SHA256_FROM_THE_RELEASE_MANIFEST"
+)
+```
+
+Aliases such as `latest` are rejected. A lookup never downloads a missing
+bundle or falls back to a different version.
+
+## Use the `_ng` interface
+
+Every data-dependent next-generation function takes the bundle first:
+
+```{r, eval = FALSE}
+reverse_zipcode_ng(bundle, c("08731", "08999", "08731"))
+geocode_zip_ng(bundle, c("08731", "08999", "08731"))
+search_radius_ng(bundle, 39.9, -74.3, radius = 10)
+zip_distance_ng(bundle, "08731", "08901")
+get_tracts_ng(bundle, "08731")
+get_cd_ng(bundle, "08731")
+```
+
+These functions preserve input order and duplicates, make missing records
+explicit, validate inputs consistently, keep geographic identifiers as
+characters, and use the bundle's declared authoritative mappings and vintage.
+USPS-only ZIPs without an authoritative district relationship remain unmapped
+with a quality reason.
+
+## Recording provenance in research outputs
+
+Store the complete version record, not merely the human-readable version:
+
+```{r, eval = FALSE}
+version_record <- zip_data_version(bundle)
+saveRDS(version_record, "results/zipcodeR-data-version.rds")
+
+version_record$data_version
+version_record$bundle_sha256
+zip_data_provenance(bundle, dataset = "zip_to_cd", key = "08731")
+```
+
+For a reproducibility supplement, retain the release manifest, the bundle, and
+the analysis code together. The verified `bundle_sha256` attribute is added by
+`read_zip_data_bundle()` and `download_zip_data_bundle()` and is propagated to
+`_ng` lookup results.
diff --git a/vignettes/faq.Rmd b/vignettes/faq.Rmd
new file mode 100644
index 0000000..764c395
--- /dev/null
+++ b/vignettes/faq.Rmd
@@ -0,0 +1,124 @@
+---
+title: "Frequently Asked Questions"
+output: rmarkdown::html_vignette
+vignette: >
+ %\VignetteIndexEntry{Frequently Asked Questions}
+ %\VignetteEngine{knitr::rmarkdown}
+ %\VignetteEncoding{UTF-8}
+---
+
+```{r, include = FALSE}
+knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
+library(zipcodeR)
+```
+
+## Why are the longitude values negative?
+
+Because the United States is in the western hemisphere. Geographic
+coordinates follow a standard sign convention: latitudes north of the
+equator are positive, and longitudes **west of the prime meridian are
+negative**. Every location in the U.S. therefore has a negative
+longitude:
+
+```{r}
+geocode_zip("90210")
+```
+
+This is not an error, and you should not multiply `lng` by -1: mapping
+libraries, spatial packages, and web APIs all expect this convention.
+
+## Why is a valid ZIP code missing from `zip_code_db`?
+
+Two common reasons:
+
+1. **Data vintage.** The bundled database is the frozen 0.3.5 snapshot, so ZIP
+ codes introduced after its date are intentionally absent. For new analyses,
+ choose a modern bundle and use `reverse_zipcode_ng()` or another `_ng`
+ function. Check `zip_data_version()` (or `zip_data_version(bundle)`) to
+ identify the selected contract.
+2. **ZIP codes are not ZCTAs.** USPS ZIP codes are collections of
+ delivery routes and points, not areas. The Census Bureau's ZIP Code
+ Tabulation Areas (ZCTAs) approximate most — but not all — ZIP codes.
+ Some USPS-only codes (typically P.O. Box or single-building "unique"
+ codes, such as 91230 in Glendale, CA) have no ZCTA and no
+ Census-derived attributes, and may be missing from ZCTA-oriented
+ sources. Use `is_zcta()` to check whether a given ZIP code is also a
+ ZCTA.
+
+Relatedly, about 20% of the ZIP codes in `zip_code_db` (P.O. Box and
+unique codes) have no `lat`/`lng` coordinates; functions such as
+`zip_distance()` return `NA` distances for them.
+
+## How accurate is the city / county ("jurisdiction") information?
+
+Treat it as approximate. A ZIP code is a postal-delivery construct, not
+a municipal boundary: one ZIP code can cross city, county, and even
+state lines, and the USPS "city" name is the preferred *mailing* name,
+which frequently differs from the municipality a given address actually
+lies in (addresses just outside a city's limits often carry that city's
+name). The `county` column reflects the predominant county for the ZIP
+code. If you need authoritative jurisdiction assignment, geocode the
+full street address against boundary files (e.g., Census TIGER/Line)
+rather than relying on the ZIP code alone.
+
+## I get `object 'zip_code_db' not found`
+
+The datasets are lazy-loaded with the package. This error almost always
+indicates a broken or partial installation — for example, a package
+library that was migrated between R versions. Reinstalling usually
+fixes it:
+
+```r
+install.packages("zipcodeR")
+```
+
+If you want the data without attaching the package, use
+`zipcodeR::zip_code_db`.
+
+## How do I get county FIPS codes from a ZIP code?
+
+For a new analysis, use `reverse_zipcode_ng()` with an explicitly selected
+bundle. Its output includes `state_fips` (two digits) and `county_fips` (the
+full five-digit county identifier) for the predominant county recorded for the
+ZIP-level row. Remember that a ZIP can cross county boundaries; this is not a
+point-level jurisdiction assignment.
+
+The unsuffixed `reverse_zipcode()` keeps its historical 0.3.5 column schema so
+an existing script is not changed by a package upgrade.
+
+## Why does zipcodeR still install raster and tidycensus?
+
+They remain installation dependencies because some historical functions need
+them to reproduce exact 0.3.5 results. They are no longer loaded by
+`library(zipcodeR)`: a legacy function loads the needed namespace only when it
+is called, while the recommended `_ng` functions avoid those paths. This keeps
+package startup isolated from the former `raster`/`terra` and GDAL/Arrow load
+failures without changing an old distance calculation.
+
+## Where does the data come from?
+
+`zip_code_db` derives from the
+[uszipcode](https://github.com/MacHu-GWU/uszipcode-project) project's
+database, which aggregates U.S. Census Bureau data (decennial census,
+ACS, gazetteer files) with USPS-derived ZIP code attributes.
+The three bundled datasets are the exact snapshots distributed by zipcodeR
+0.3.5: the 2021 uszipcode-derived ZIP database, the Census 2010
+ZCTA-to-tract relationship, and the pre-2020 HUD-USPS ZIP-to-district
+relationship. They are intentionally frozen so a legacy call cannot be
+retconned by a package upgrade. `zip_data_version()` reports this contract.
+
+Newer Census relationships and refreshed attributes are distributed
+as separate, checksum-pinned data bundles. They are used only when a bundle is
+passed explicitly to an `_ng` function. The bundle's manifest and
+reproducibility archive contain the source vintages, raw source hashes,
+licenses, pipeline commit, dependency lock, validation report, and quality
+sidecars. No USPS-only ZIP receives a city-derived proxy centroid or a
+city-wide inferred congressional district.
+
+## Which API should a new project use?
+
+Use `_ng` functions with a named data bundle for new analyses. This is the
+forward-looking interface and makes the choice of newer data visible in the R
+code. It does not silently track future releases: keep the exact version and
+SHA recorded with the project. Keep using unsuffixed functions for existing
+scripts or when reproducing historical zipcodeR results.