From 4dcf1c72b22bfa6045144a931402146023584477 Mon Sep 17 00:00:00 2001 From: Jared Knowles Date: Tue, 28 Apr 2026 11:05:14 -0400 Subject: [PATCH] =?UTF-8?q?feat(search):=20add=20per-row=20resolver=20?= =?UTF-8?q?=E2=80=94=20exact=20match=20branch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .resolve_basket_row() handles the exact-match case. Substring fallback and disambiguation branches follow in subsequent commits. --- R/search.R | 50 ++++++++++++++++++++++++++++++++++++ tests/testthat/test-search.R | 32 +++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/R/search.R b/R/search.R index 3b1320b..b2b5127 100644 --- a/R/search.R +++ b/R/search.R @@ -158,3 +158,53 @@ cog_gov_search <- function(name = NULL, state = NULL, type = NULL) { list(name = name, state = state_norm, type = type_norm) } + +# Resolve a single basket-mode input row. Returns a list with components: +# status : "resolved" | "largest_pop" | "ambiguous" | "no_match" +# match_method : "exact" | "substring" | NA_character_ +# n_candidates : int +# row : tibble (single resolved row, or 0-row tibble for unresolved) +# candidates : tibble (all rows that matched, for sidecar) +# Internal use only; takes an active DuckDB connection to reuse the session. +#' @noRd +.resolve_basket_row <- function(name, state, type, con) { + preds <- character(0) + if (!is.na(state)) { + st_fips <- .coerce_state_to_fips(state) + preds <- c(preds, sprintf("fips_state = %s", .sql_lit_chr(st_fips))) + } + if (!is.na(type)) { + int_type <- .coerce_type(type) + preds <- c(preds, sprintf("govs_type = %d", int_type)) + } + + base_where <- if (length(preds) == 0L) "" else paste("WHERE", paste(preds, collapse = " AND ")) + + exact_sql <- paste( + "SELECT * FROM canonical_fips_xwalk", + base_where, + if (nzchar(base_where)) "AND" else "WHERE", + sprintf("LOWER(gov_name) = LOWER(%s)", .sql_lit_chr(name)) + ) + exact <- tibble::as_tibble(DBI::dbGetQuery(con, exact_sql)) + + if (nrow(exact) == 1L) { + return(list( + status = "resolved", + match_method = "exact", + n_candidates = 1L, + row = exact, + candidates = exact + )) + } + + # Substring + disambiguation branches added in subsequent tasks. + empty <- exact[0, , drop = FALSE] + list( + status = "no_match", + match_method = NA_character_, + n_candidates = 0L, + row = empty, + candidates = empty + ) +} diff --git a/tests/testthat/test-search.R b/tests/testthat/test-search.R index ea19a5f..b5f68b3 100644 --- a/tests/testthat/test-search.R +++ b/tests/testthat/test-search.R @@ -136,3 +136,35 @@ test_that(".validate_basket_args allows NULL state and type", { expect_equal(out$state, c(NA_character_, NA_character_)) expect_equal(out$type, c(NA_character_, NA_character_)) }) + +test_that(".resolve_basket_row exact match returns one row", { + con <- uscogdata:::.ensure_session() + out <- uscogdata:::.resolve_basket_row( + name = "BROWARD COUNTY", state = "FL", type = NA_character_, con = con + ) + expect_equal(out$status, "resolved") + expect_equal(out$match_method, "exact") + expect_equal(out$n_candidates, 1L) + expect_equal(nrow(out$row), 1L) + expect_equal(out$row$canonical_govid, "101006006") + expect_equal(out$row$gov_name, "BROWARD COUNTY") +}) + +test_that(".resolve_basket_row exact match is case-insensitive", { + con <- uscogdata:::.ensure_session() + out <- uscogdata:::.resolve_basket_row( + name = "broward county", state = "FL", type = NA_character_, con = con + ) + expect_equal(out$status, "resolved") + expect_equal(out$match_method, "exact") + expect_equal(out$row$canonical_govid, "101006006") +}) + +test_that(".resolve_basket_row exact match honors per-row type", { + con <- uscogdata:::.ensure_session() + out <- uscogdata:::.resolve_basket_row( + name = "SAN DIEGO CITY", state = "CA", type = "city", con = con + ) + expect_equal(out$status, "resolved") + expect_equal(out$row$canonical_govid, "052037010") +})