diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 0000000..885398e --- /dev/null +++ b/NEWS.md @@ -0,0 +1,21 @@ +# uscogdata 0.1.0 (development) + +## New features + +* `cog_gov_search()` gains a **basket mode**: passing vector `name` + / `state` / `type` arguments resolves multiple place names in one + call and returns a tibble of canonical rows in input order, ready + to pipe into `cog_spending()` / `cog_revenue()`. Per-row resolution + follows an exact-then-substring matching algorithm with deterministic + disambiguation; ambiguous and missing entries are surfaced via a + sidecar audit tibble plus a single console summary message. +* New exports `cog_basket_resolution()` and `cog_basket_unresolved()` + expose the basket sidecar for iterative query refinement. + +## Breaking changes + +* The first formal of `cog_gov_search()` was renamed from `pattern` + to `name`. All existing call sites in `cog_explorer/` and the + package itself use positional first-arg, so this rename is + non-breaking in practice. Callers that pass `pattern = ...` by name + must update to `name = ...`. diff --git a/R/basket.R b/R/basket.R index c6505cd..bca6985 100644 --- a/R/basket.R +++ b/R/basket.R @@ -86,6 +86,15 @@ #' list-column (full-schema match candidates per input row). Default #' `FALSE`. #' @return A tibble with the resolution audit trail. +#' @examples +#' \dontrun{ +#' basket <- cog_gov_search( +#' name = c("Broward", "San Diego", "Notarealplace"), +#' state = c("FL", "CA", "NY") +#' ) +#' cog_basket_resolution(basket) +#' cog_basket_resolution(basket, expand_candidates = TRUE) +#' } #' @export cog_basket_resolution <- function(x, expand_candidates = FALSE) { res <- attr(x, "resolution") @@ -111,6 +120,14 @@ cog_basket_resolution <- function(x, expand_candidates = FALSE) { #' #' @param x A tibble returned by basket-mode [cog_gov_search()]. #' @return A tibble (subset of [cog_basket_resolution()]). +#' @examples +#' \dontrun{ +#' basket <- cog_gov_search( +#' name = c("Broward", "San Diego", "Notarealplace"), +#' state = c("FL", "CA", "NY") +#' ) +#' cog_basket_unresolved(basket) +#' } #' @export cog_basket_unresolved <- function(x) { res <- cog_basket_resolution(x, expand_candidates = TRUE) diff --git a/R/search.R b/R/search.R index 83f0c6c..b0b7da9 100644 --- a/R/search.R +++ b/R/search.R @@ -2,29 +2,81 @@ #' Search for governments by name, state, and/or type #' -#' Two modes: +#' Resolves human-readable place names into rows of `canonical_fips_xwalk`, +#' the cross-vintage canonical-government registry. Operates in two modes: #' -#' * **Utility mode** (single `name`): returns all rows from -#' `canonical_fips_xwalk` whose `gov_name` matches the regex -#' case-insensitively, sorted by `population_acs` descending. +#' * **Utility mode** (single `name`, the original behavior): returns all +#' rows whose `gov_name` matches the regex case-insensitively, sorted by +#' `population_acs` descending. Useful for exploratory lookups. #' * **Basket mode** (`length(name) > 1`): resolves each input row to a -#' single canonical govid via exact-then-substring matching with -#' deterministic disambiguation. Returns up to `length(name)` rows in -#' input order plus a `"resolution"` sidecar attribute. See -#' [cog_basket_resolution()]. +#' single canonical govid and returns a tibble in input order, suitable +#' for piping straight into [cog_spending()] / [cog_revenue()] / +#' [cog_geographic_rollup()]. Carries an audit sidecar accessible via +#' [cog_basket_resolution()] / [cog_basket_unresolved()]. +#' +#' @details +#' **Basket-mode resolution algorithm** (per input row): +#' 1. Filter `canonical_fips_xwalk` by `state` and (if non-NA) `type`. +#' 2. **Exact pass:** case-insensitive equality against `gov_name`. +#' Single hit -> resolved. Multiple -> step 4. +#' 3. **Substring fallback:** case-insensitive regex against `gov_name`. +#' Single hit -> resolved (`match_method = "substring"`). Zero hits -> +#' `status = "no_match"`. Multiple hits -> step 4. +#' 4. **Disambiguation:** if matches share one `govs_type`, pick the +#' largest-population row (`status = "largest_pop"`). If they span >=2 +#' types, no row is added (`status = "ambiguous"`); the user should +#' re-run with `type` specified. +#' +#' Resolved rows form the returned tibble in input order. Unresolved +#' inputs (`ambiguous` / `no_match`) appear only in the sidecar. #' #' @param name Character vector of place name(s). Length 1 = utility mode; #' length >1 = basket mode. -#' @param state Either a 2-letter USPS abbreviation, a FIPS integer, or -#' `NULL`. Length 1 recycles across all entries in basket mode. -#' @param type Government type: an integer in `0:3` or one of `"state"`, -#' `"county"`, `"city"`, `"township"`, or `NA` (per-row optional in -#' basket mode). Passing `4`, `5`, `"special_district"`, or -#' `"school_district"` emits an explanatory message and returns an -#' empty tibble (v0.1 corpus excludes those types). -#' @return Tibble from `canonical_fips_xwalk`. In utility mode, sorted by -#' `population_acs` descending (`NULL`s last). In basket mode, in input -#' order, with `attr(result, "resolution")` set to the sidecar tibble. +#' @param state 2-letter USPS abbreviation (e.g. `"FL"`), FIPS integer +#' (e.g. `12`), or `NULL`. In basket mode, length 1 recycles across +#' all entries; otherwise must match `length(name)`. +#' @param type Government type: integer in `0:3` or one of `"state"`, +#' `"county"`, `"city"`, `"township"`, or `NA`/`NULL`. Per-row optional +#' in basket mode (recycles from length 1). Excluded types `4`/`5` (or +#' `"special_district"` / `"school_district"`) trigger an explanatory +#' message and an empty result. +#' @return A tibble of `canonical_fips_xwalk` rows. In utility mode, all +#' matches sorted by `population_acs` desc. In basket mode, resolved +#' rows in input order, with `attr(., "resolution")` set to the +#' sidecar tibble. +#' @seealso [cog_basket_resolution()], [cog_basket_unresolved()], +#' [cog_spending()], [cog_revenue()]. +#' @examples +#' \dontrun{ +#' # Utility mode — exploratory regex lookup +#' cog_gov_search("broward", state = "FL") +#' +#' # Basket mode — resolve a known cohort +#' basket <- cog_gov_search( +#' name = c("BROWARD COUNTY", "SAN DIEGO CITY", "AUSTIN CITY"), +#' state = c("FL", "CA", "TX") +#' ) +#' basket +#' +#' # Inspect resolution audit +#' cog_basket_resolution(basket) +#' +#' # Pipe into a spending query +#' library(dplyr) +#' basket |> cog_spending(years = 2019:2020, category = "Police") +#' +#' # Iteratively refine ambiguous matches +#' partial <- cog_gov_search( +#' name = c("Broward", "San Diego"), # San Diego is ambiguous +#' state = c("FL", "CA") +#' ) +#' cog_basket_unresolved(partial) +#' refined <- cog_gov_search( +#' name = c("Broward", "San Diego"), +#' state = c("FL", "CA"), +#' type = c(NA, "city") # disambiguate +#' ) +#' } #' @export cog_gov_search <- function(name = NULL, state = NULL, type = NULL) { if (!is.null(type) && length(type) == 1L && .is_excluded_type(type)) { diff --git a/_pkgdown.yml b/_pkgdown.yml index 3aeefe7..2022548 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -3,6 +3,12 @@ template: bootstrap: 5 reference: + - title: Search & basket + desc: Resolve place names into canonical govids. + contents: + - cog_gov_search + - cog_basket_resolution + - cog_basket_unresolved - title: Session contents: - has_keyword("internal") diff --git a/man/cog_basket_resolution.Rd b/man/cog_basket_resolution.Rd index 6ad2f24..3c1825f 100644 --- a/man/cog_basket_resolution.Rd +++ b/man/cog_basket_resolution.Rd @@ -23,3 +23,13 @@ Returns the resolution tibble attached to a basket-mode result of the `candidates` list-column is dropped for readable printing; pass `expand_candidates = TRUE` to keep it. } +\examples{ +\dontrun{ +basket <- cog_gov_search( + name = c("Broward", "San Diego", "Notarealplace"), + state = c("FL", "CA", "NY") +) +cog_basket_resolution(basket) +cog_basket_resolution(basket, expand_candidates = TRUE) +} +} diff --git a/man/cog_basket_unresolved.Rd b/man/cog_basket_unresolved.Rd index 727764c..e6a5145 100644 --- a/man/cog_basket_unresolved.Rd +++ b/man/cog_basket_unresolved.Rd @@ -18,3 +18,12 @@ Convenience wrapper that returns just the rows where `status` is refine before piping into a query verb. The `candidates` list-column is preserved so the user can drill into ambiguous match sets. } +\examples{ +\dontrun{ +basket <- cog_gov_search( + name = c("Broward", "San Diego", "Notarealplace"), + state = c("FL", "CA", "NY") +) +cog_basket_unresolved(basket) +} +} diff --git a/man/cog_gov_search.Rd b/man/cog_gov_search.Rd index 0d2a0e6..4a6b55b 100644 --- a/man/cog_gov_search.Rd +++ b/man/cog_gov_search.Rd @@ -7,24 +7,88 @@ cog_gov_search(name = NULL, state = NULL, type = NULL) } \arguments{ -\item{name}{Character regex matched case-insensitively against -`gov_name`. `NULL` (default) means no name filter.} +\item{name}{Character vector of place name(s). Length 1 = utility mode; +length >1 = basket mode.} -\item{state}{Either a 2-letter USPS abbreviation (e.g. `"FL"`), a FIPS -integer (e.g. `12`), or `NULL`.} +\item{state}{2-letter USPS abbreviation (e.g. `"FL"`), FIPS integer +(e.g. `12`), or `NULL`. In basket mode, length 1 recycles across +all entries; otherwise must match `length(name)`.} -\item{type}{Government type: an integer in `0:3` or one of `"state"`, -`"county"`, `"city"`, `"township"`. Passing `4`, `5`, -`"special_district"`, or `"school_district"` emits an explanatory -message and returns an empty tibble (v0.1 corpus excludes those types).} +\item{type}{Government type: integer in `0:3` or one of `"state"`, +`"county"`, `"city"`, `"township"`, or `NA`/`NULL`. Per-row optional +in basket mode (recycles from length 1). Excluded types `4`/`5` (or +`"special_district"` / `"school_district"`) trigger an explanatory +message and an empty result.} } \value{ -Tibble from `canonical_fips_xwalk` sorted by `population_acs` - descending (`NULL`s last). +A tibble of `canonical_fips_xwalk` rows. In utility mode, all + matches sorted by `population_acs` desc. In basket mode, resolved + rows in input order, with `attr(., "resolution")` set to the + sidecar tibble. } \description{ -Returns rows from `canonical_fips_xwalk` matching the supplied filters. -Intended as the entry point users call to resolve a human-readable place -name into one or more `canonical_govid` values before calling -[cog_spending()] / [cog_revenue()] / etc. +Resolves human-readable place names into rows of `canonical_fips_xwalk`, +the cross-vintage canonical-government registry. Operates in two modes: +} +\details{ +* **Utility mode** (single `name`, the original behavior): returns all + rows whose `gov_name` matches the regex case-insensitively, sorted by + `population_acs` descending. Useful for exploratory lookups. +* **Basket mode** (`length(name) > 1`): resolves each input row to a + single canonical govid and returns a tibble in input order, suitable + for piping straight into [cog_spending()] / [cog_revenue()] / + [cog_geographic_rollup()]. Carries an audit sidecar accessible via + [cog_basket_resolution()] / [cog_basket_unresolved()]. + + +**Basket-mode resolution algorithm** (per input row): +1. Filter `canonical_fips_xwalk` by `state` and (if non-NA) `type`. +2. **Exact pass:** case-insensitive equality against `gov_name`. + Single hit -> resolved. Multiple -> step 4. +3. **Substring fallback:** case-insensitive regex against `gov_name`. + Single hit -> resolved (`match_method = "substring"`). Zero hits -> + `status = "no_match"`. Multiple hits -> step 4. +4. **Disambiguation:** if matches share one `govs_type`, pick the + largest-population row (`status = "largest_pop"`). If they span >=2 + types, no row is added (`status = "ambiguous"`); the user should + re-run with `type` specified. + +Resolved rows form the returned tibble in input order. Unresolved +inputs (`ambiguous` / `no_match`) appear only in the sidecar. +} +\examples{ +\dontrun{ +# Utility mode — exploratory regex lookup +cog_gov_search("broward", state = "FL") + +# Basket mode — resolve a known cohort +basket <- cog_gov_search( + name = c("BROWARD COUNTY", "SAN DIEGO CITY", "AUSTIN CITY"), + state = c("FL", "CA", "TX") +) +basket + +# Inspect resolution audit +cog_basket_resolution(basket) + +# Pipe into a spending query +library(dplyr) +basket |> cog_spending(years = 2019:2020, category = "Police") + +# Iteratively refine ambiguous matches +partial <- cog_gov_search( + name = c("Broward", "San Diego"), # San Diego is ambiguous + state = c("FL", "CA") +) +cog_basket_unresolved(partial) +refined <- cog_gov_search( + name = c("Broward", "San Diego"), + state = c("FL", "CA"), + type = c(NA, "city") # disambiguate +) +} +} +\seealso{ +[cog_basket_resolution()], [cog_basket_unresolved()], + [cog_spending()], [cog_revenue()]. }