feat(search): add substring fallback and no_match handling
.resolve_basket_row() now falls back to case-insensitive substring match when no exact match is found, and short-circuits empty/whitespace input to no_match. Disambiguation stub raises pending Task 5.
This commit is contained in:
+47
-8
@@ -168,6 +168,18 @@ cog_gov_search <- function(name = NULL, state = NULL, type = NULL) {
|
|||||||
# Internal use only; takes an active DuckDB connection to reuse the session.
|
# Internal use only; takes an active DuckDB connection to reuse the session.
|
||||||
#' @noRd
|
#' @noRd
|
||||||
.resolve_basket_row <- function(name, state, type, con) {
|
.resolve_basket_row <- function(name, state, type, con) {
|
||||||
|
# Short-circuit: empty/whitespace name -> no_match without SQL.
|
||||||
|
if (!nzchar(trimws(name))) {
|
||||||
|
empty <- .empty_xwalk_tibble()
|
||||||
|
return(list(
|
||||||
|
status = "no_match",
|
||||||
|
match_method = NA_character_,
|
||||||
|
n_candidates = 0L,
|
||||||
|
row = empty,
|
||||||
|
candidates = empty
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
preds <- character(0)
|
preds <- character(0)
|
||||||
if (!is.na(state)) {
|
if (!is.na(state)) {
|
||||||
st_fips <- .coerce_state_to_fips(state)
|
st_fips <- .coerce_state_to_fips(state)
|
||||||
@@ -177,13 +189,13 @@ cog_gov_search <- function(name = NULL, state = NULL, type = NULL) {
|
|||||||
int_type <- .coerce_type(type)
|
int_type <- .coerce_type(type)
|
||||||
preds <- c(preds, sprintf("govs_type = %d", int_type))
|
preds <- c(preds, sprintf("govs_type = %d", int_type))
|
||||||
}
|
}
|
||||||
|
|
||||||
base_where <- if (length(preds) == 0L) "" else paste("WHERE", paste(preds, collapse = " AND "))
|
base_where <- if (length(preds) == 0L) "" else paste("WHERE", paste(preds, collapse = " AND "))
|
||||||
|
conj <- if (nzchar(base_where)) "AND" else "WHERE"
|
||||||
|
|
||||||
exact_sql <- paste(
|
exact_sql <- paste(
|
||||||
"SELECT * FROM canonical_fips_xwalk",
|
"SELECT * FROM canonical_fips_xwalk",
|
||||||
base_where,
|
base_where,
|
||||||
if (nzchar(base_where)) "AND" else "WHERE",
|
conj,
|
||||||
sprintf("LOWER(gov_name) = LOWER(%s)", .sql_lit_chr(name))
|
sprintf("LOWER(gov_name) = LOWER(%s)", .sql_lit_chr(name))
|
||||||
)
|
)
|
||||||
exact <- tibble::as_tibble(DBI::dbGetQuery(con, exact_sql))
|
exact <- tibble::as_tibble(DBI::dbGetQuery(con, exact_sql))
|
||||||
@@ -197,14 +209,41 @@ cog_gov_search <- function(name = NULL, state = NULL, type = NULL) {
|
|||||||
candidates = exact
|
candidates = exact
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
if (nrow(exact) > 1L) {
|
||||||
|
return(.disambiguate(exact, method = "exact"))
|
||||||
|
}
|
||||||
|
|
||||||
# Substring + disambiguation branches added in subsequent tasks.
|
sub_sql <- paste(
|
||||||
empty <- exact[0, , drop = FALSE]
|
"SELECT * FROM canonical_fips_xwalk",
|
||||||
list(
|
base_where,
|
||||||
|
conj,
|
||||||
|
sprintf("regexp_matches(gov_name, %s, 'i')", .sql_lit_chr(name))
|
||||||
|
)
|
||||||
|
sub <- tibble::as_tibble(DBI::dbGetQuery(con, sub_sql))
|
||||||
|
|
||||||
|
if (nrow(sub) == 0L) {
|
||||||
|
return(list(
|
||||||
status = "no_match",
|
status = "no_match",
|
||||||
match_method = NA_character_,
|
match_method = NA_character_,
|
||||||
n_candidates = 0L,
|
n_candidates = 0L,
|
||||||
row = empty,
|
row = sub,
|
||||||
candidates = empty
|
candidates = sub
|
||||||
)
|
))
|
||||||
|
}
|
||||||
|
if (nrow(sub) == 1L) {
|
||||||
|
return(list(
|
||||||
|
status = "resolved",
|
||||||
|
match_method = "substring",
|
||||||
|
n_candidates = 1L,
|
||||||
|
row = sub,
|
||||||
|
candidates = sub
|
||||||
|
))
|
||||||
|
}
|
||||||
|
.disambiguate(sub, method = "substring")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stub for Task 5; raises so any accidental hit during Task 4 is loud.
|
||||||
|
#' @noRd
|
||||||
|
.disambiguate <- function(matches, method) {
|
||||||
|
cli::cli_abort("internal: .disambiguate() not yet implemented")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,3 +168,39 @@ test_that(".resolve_basket_row exact match honors per-row type", {
|
|||||||
expect_equal(out$status, "resolved")
|
expect_equal(out$status, "resolved")
|
||||||
expect_equal(out$row$canonical_govid, "052037010")
|
expect_equal(out$row$canonical_govid, "052037010")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test_that(".resolve_basket_row substring fallback resolves single match", {
|
||||||
|
con <- uscogdata:::.ensure_session()
|
||||||
|
out <- uscogdata:::.resolve_basket_row(
|
||||||
|
name = "Broward", state = "FL", type = NA_character_, con = con
|
||||||
|
)
|
||||||
|
expect_equal(out$status, "resolved")
|
||||||
|
expect_equal(out$match_method, "substring")
|
||||||
|
expect_equal(out$n_candidates, 1L)
|
||||||
|
expect_equal(out$row$canonical_govid, "101006006")
|
||||||
|
})
|
||||||
|
|
||||||
|
test_that(".resolve_basket_row no_match returns 0-row tibble", {
|
||||||
|
con <- uscogdata:::.ensure_session()
|
||||||
|
out <- uscogdata:::.resolve_basket_row(
|
||||||
|
name = "Notarealplace", state = "NY", type = NA_character_, con = con
|
||||||
|
)
|
||||||
|
expect_equal(out$status, "no_match")
|
||||||
|
expect_true(is.na(out$match_method))
|
||||||
|
expect_equal(out$n_candidates, 0L)
|
||||||
|
expect_equal(nrow(out$row), 0L)
|
||||||
|
expect_equal(nrow(out$candidates), 0L)
|
||||||
|
})
|
||||||
|
|
||||||
|
test_that(".resolve_basket_row treats empty/whitespace name as no_match", {
|
||||||
|
con <- uscogdata:::.ensure_session()
|
||||||
|
out_empty <- uscogdata:::.resolve_basket_row(
|
||||||
|
name = "", state = "FL", type = NA_character_, con = con
|
||||||
|
)
|
||||||
|
expect_equal(out_empty$status, "no_match")
|
||||||
|
|
||||||
|
out_ws <- uscogdata:::.resolve_basket_row(
|
||||||
|
name = " ", state = "FL", type = NA_character_, con = con
|
||||||
|
)
|
||||||
|
expect_equal(out_ws$status, "no_match")
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user