R is for
Authoring!!

OOH Session #142…
typst  
INVOICE  

Open Office Hours
(August 11, 2026)

  • Recap session #141
  • Today’s topic(s):
  • Shared problem-solving






Reminder – check it out!!

Fantastic resource!!

Breaking News!!

Recap of Session
#141:

AI–Assisted App Build (III)

app.R:

# app.R - standalone Shiny app that geocodes an address, shows it on a leaflet map,
# displays lat/lon, matched address, and a small Census tract population (if available).
# Updated: show ACS 'NAME' (pop$name) when available; attempt coords->geographies fallback when needed.

library(shiny)
library(leaflet)

# Load geocode and Census helper
source("R/helpers.R")

ui <- fluidPage(
  titlePanel("Address → Map with info"),
  sidebarLayout(
    sidebarPanel(
      textInput("address", "Type an address (one line):", value = "1600 Pennsylvania Ave NW, Washington, DC 20500"),
      actionButton("go", "Locate address"),
      br(), br(),
      tags$b("Parsed location"),
      br(),
      verbatimTextOutput("latlon", placeholder = TRUE),
      verbatimTextOutput("matched", placeholder = TRUE),
      verbatimTextOutput("pop_name", placeholder = TRUE),
      verbatimTextOutput("tract_info", placeholder = TRUE),
      br(),
      tags$div(
        tags$small("If the app reports \"Census key not set\" for tract info, you can add a Census API key here: "),
        actionButton("set_key_btn", "Set Census API key"),
        tags$br(),
        tags$small("Get a free Census API key: "),
        tags$a(href = "https://api.census.gov/data/key_signup.html", "Census API key signup", target = "_blank")
      ),
      br(),
      tags$small("Debug log:"),
      verbatimTextOutput("debug", placeholder = TRUE)
    ),
    mainPanel(
      leafletOutput("map", height = "600px")
    )
  )
)

server <- function(input, output, session) {

  # simple in-memory debug messages
  msgs <- reactiveVal(character())
  log <- function(...) {
    new <- paste0(Sys.time(), " - ", paste0(..., collapse = " "))
    msgs(c(msgs(), new))
    message(new)
  }

  output$debug <- renderText({ paste(msgs(), collapse = "\n") })

  output$map <- renderLeaflet({
    leaflet() %>% addTiles() %>% setView(lng = -98.5795, lat = 39.8283, zoom = 4)
  })

  # display parsed lat/lon and matched address
  latlon_val <- reactiveVal(NULL)
  matched_val <- reactiveVal(NULL)
  tract_val <- reactiveVal(NULL)
  pop_name_val <- reactiveVal(NULL)

  output$latlon <- renderText({
    v <- latlon_val()
    if (is.null(v)) return("lat: -, lon: -")
    paste0("lat: ", v$lat, " , lon: ", v$lon)
  })
  output$matched <- renderText({
    v <- matched_val()
    if (is.null(v)) return("matched: -")
    paste0("matched: ", v)
  })
  output$pop_name <- renderText({
    v <- pop_name_val()
    if (is.null(v)) return("Place name (ACS NAME): -")
    paste0("Place name (ACS NAME): ", v)
  })
  output$tract_info <- renderText({
    v <- tract_val()
    if (is.null(v)) return("tract: -")
    if (!is.null(v$status) && v$status == "ok") {
      paste0("Tract GEOID: ", v$geoid, "\nPopulation (ACS5): ", v$population, "\nName: ", v$name)
    } else if (!is.null(v$status) && v$status == "no_key") {
      paste0("Tract GEOID: ", v$geoid, " (Census key not set; population not fetched). Click 'Set Census API key' to enter a key and fetch.")
    } else {
      paste0("Tract lookup: ", v$message)
    }
  })

  # Nominatim fallback
  nominatim_one <- function(address) {
    url <- "https://nominatim.openstreetmap.org/search"
    res <- tryCatch(
      httr::GET(url, query = list(q = address, format = "json", limit = 1), httr::user_agent("TommyBoy-App/1.0"), httr::timeout(10)),
      error = function(e) NULL
    )
    if (is.null(res) || httr::status_code(res) != 200) return(NULL)
    parsed <- httr::content(res, as = "parsed")
    if (length(parsed) == 0) return(NULL)
    d <- parsed[[1]]
    list(lat = as.numeric(d$lat), lon = as.numeric(d$lon), label = d$display_name, raw = d)
  }

  # Extract tract GEOID from the 'raw' match object returned by Census geocoder - fallback to coords geographies
  extract_tract_geoid <- function(raw_match) {
    if (is.null(raw_match)) return(NULL)
    geogs <- raw_match$geographies
    if (!is.null(geogs) && !is.null(geogs$`Census Tracts`) && length(geogs$`Census Tracts`) > 0) {
      ct <- geogs$`Census Tracts`[[1]]
      if (!is.null(ct$GEOID)) return(ct$GEOID)
    }
    return(NULL)
  }

  # Helper to attempt tract population fetch if geoid present and key available
  try_fetch_tract <- function(tract_geoid) {
    pop_name_val(NULL)
    if (is.null(tract_geoid)) return(NULL)
    key <- Sys.getenv("CENSUS_API_KEY", "")
    if (!nzchar(key)) {
      tract_val(list(status = "no_key", geoid = tract_geoid))
      return(NULL)
    }
    pop_res <- fetch_acs_population(tract_geoid, year = 2021, key = key)
    if (!is.null(pop_res) && is.list(pop_res) && identical(pop_res$status, "ok")) {
      tract_val(list(status = "ok", geoid = tract_geoid, population = pop_res$population, name = pop_res$name))
      pop_name_val(pop_res$name)
    } else {
      tract_val(list(status = "error", message = if (!is.null(pop_res$message)) pop_res$message else "Census fetch failed", geoid = tract_geoid))
      # if pop_res has a name even when status != ok, try to display it
      if (!is.null(pop_res$name)) pop_name_val(pop_res$name)
    }
  }

  observeEvent(input$go, {
    addr <- trimws(input$address)
    log("Locate button clicked. Address:", addr)
    if (addr == "" || is.null(addr)) { log("Empty address"); showNotification("Please enter an address.", type = "warning"); return() }

    # Primary: use Census geocoder
    res_obj <- NULL
    res_obj <- tryCatch(geocode_census_one(addr), error = function(e) { log("geocode_census_one error:", e$message); list(status = "error", message = e$message) })

    if (!is.null(res_obj) && is.list(res_obj) && !is.null(res_obj$status) && res_obj$status == "ok") {
      # success
      log(paste0("Census geocoder returned:lat=", res_obj$lat, " lon=", res_obj$lon, " label=", res_obj$label))
      latlon_val(list(lat = res_obj$lat, lon = res_obj$lon))
      matched_val(res_obj$label)
      pop_name_val(NULL)

      # Update map
      leafletProxy("map") %>% clearMarkers() %>% addMarkers(lng = res_obj$lon, lat = res_obj$lat, popup = res_obj$label) %>% setView(lng = res_obj$lon, lat = res_obj$lat, zoom = 16)

      # Try to extract tract GEOID from raw response
      tract_geoid <- extract_tract_geoid(res_obj$raw)
      if (is.null(tract_geoid)) {
        # fallback: call coordinates->geographies to obtain tracts
        geo_res <- tryCatch(geographies_for_coords(lat = res_obj$lat, lon = res_obj$lon), error = function(e) list(status = "error", message = e$message))
        if (!is.null(geo_res) && is.list(geo_res) && identical(geo_res$status, "ok")) {
          if (!is.null(geo_res$geographies$`Census Tracts`) && length(geo_res$geographies$`Census Tracts`) > 0) {
            tract_geoid <- geo_res$geographies$`Census Tracts`[[1]]$GEOID
            log(paste0("Found tract GEOID via coords->geographies: ", tract_geoid))
          }
        } else {
          log(paste0("coords->geographies lookup failed: ", if (!is.null(geo_res$message)) geo_res$message else "unknown"))
        }
      }

      if (!is.null(tract_geoid)) {
        try_fetch_tract(tract_geoid)
      } else {
        tract_val(list(status = "nomatch", message = "No tract GEOID in geocoder response"))
      }

      showNotification("Location found and map updated.", type = "message")
      return()
    }

    # If Census geocoder didn't return ok, try Nominatim fallback
    log("Census geocoder did not return ok; attempting Nominatim fallback.")
    res_nom <- nominatim_one(addr)
    if (!is.null(res_nom)) {
      log(paste0("Nominatim returned lat=", res_nom$lat, " lon=", res_nom$lon, " label=", res_nom$label))
      latlon_val(list(lat = res_nom$lat, lon = res_nom$lon))
      matched_val(res_nom$label)
      tract_val(NULL)
      pop_name_val(NULL)
      leafletProxy("map") %>% clearMarkers() %>% addMarkers(lng = res_nom$lon, lat = res_nom$lat, popup = res_nom$label) %>% setView(lng = res_nom$lon, lat = res_nom$lat, zoom = 16)
      showNotification("Location found (fallback) and map updated.", type = "message")
      return()
    }

    log("All geocoding attempts failed.")
    showNotification("No geocode result.", type = "warning")
  })

  # Modal for setting Census API key
  observeEvent(input$set_key_btn, {
    showModal(modalDialog(
      title = "Set Census API key",
      tagList(
        tags$p("If you have a Census API key, paste it here to allow fetching ACS tract data (population, name). This sets the key for the current R session."),
        tags$p(tags$a(href = "https://api.census.gov/data/key_signup.html", "Get a free Census API key", target = "_blank")),
        textInput("census_key_input", "Paste Census API key:"),
        tags$small("After saving the key, any displayed tract GEOID will attempt to fetch population automatically.")
      ),
      footer = tagList(actionButton("save_census_key", "Save key and fetch"), modalButton("Cancel")),
      easyClose = TRUE
    ))
  })

  observeEvent(input$save_census_key, {
    key_val <- input$census_key_input
    if (!is.null(key_val) && nzchar(trimws(key_val))) {
      Sys.setenv(CENSUS_API_KEY = trimws(key_val))
      log("Census API key set in session.")
      removeModal()
      showNotification("Census API key set in this R session.", type = "message")

      # If we have a tract GEOID displayed with no_key or error, try fetching now
      current_tract <- tract_val()
      if (!is.null(current_tract) && !is.null(current_tract$geoid)) {
        log(paste0("Attempting to fetch population for GEOID: ", current_tract$geoid))
        try_fetch_tract(current_tract$geoid)
      }
    } else {
      showNotification("No key entered.", type = "warning")
    }
  })

  # initial message
  observe({
    if (length(msgs()) == 0) {
      if (exists("geocode_census_one")) log("App started: geocode_census_one() available.") else log("App started: geocode_census_one() NOT available.")
    }
  })
}

shinyApp(ui, server)
1
could also be folded into app.R – “sourcing” an option for efficient scripting of primary capability. Note the subfolder location if replicating locally.

helpers.R:

# Helper functions for geocoding and simple Census API access
# Keep lightweight: only depend on httr and jsonlite to avoid heavy system deps

library(httr)
library(jsonlite)

# geocode_census_one: calls US Census onelineaddress geocoder and returns structured result
geocode_census_one <- function(address, benchmark = "Public_AR_Current", timeout_seconds = 10) {
  if (missing(address) || nchar(trimws(address)) == 0) return(list(status = "error", message = "empty address"))
  base <- "https://geocoding.geo.census.gov/geocoder/locations/onelineaddress"

  resp <- tryCatch({
    httr::GET(
      url = base,
      query = list(address = address, benchmark = benchmark, format = "json"),
      httr::user_agent("TommyBoy/1.0 (geocode_census_one)"),
      httr::timeout(timeout_seconds)
    )
  }, error = function(e) {
    list(status = "error", message = paste0("HTTP error: ", e$message))
  })

  if (is.list(resp) && !inherits(resp, "response")) return(resp)
  if (httr::status_code(resp) != 200) return(list(status = "error", message = paste0("HTTP status ", httr::status_code(resp))))

  txt <- httr::content(resp, as = "text", encoding = "UTF-8")
  parsed <- tryCatch({ jsonlite::fromJSON(txt, simplifyVector = FALSE) }, error = function(e) NULL)
  if (is.null(parsed)) return(list(status = "error", message = "JSON parse error"))

  matches <- parsed$result$addressMatches
  if (is.null(matches) || length(matches) == 0) return(list(status = "nomatch", message = "no addressMatches returned", raw = parsed$result))

  m <- matches[[1]]
  coords <- NULL
  if (!is.null(m$coordinates)) coords <- m$coordinates
  if (is.null(coords) && !is.null(m$coordinates$x) && !is.null(m$coordinates$y)) coords <- list(x = m$coordinates$x, y = m$coordinates$y)
  if (is.null(coords)) return(list(status = "error", message = "match found but coordinates missing", raw = m))

  lon <- suppressWarnings(as.numeric(coords$x))
  lat <- suppressWarnings(as.numeric(coords$y))
  if (!is.finite(lat) || !is.finite(lon)) return(list(status = "error", message = "coordinates not numeric/finite", raw = m))

  label <- if (!is.null(m$matchedAddress)) m$matchedAddress else address
  list(status = "ok", lat = lat, lon = lon, label = label, raw = m)
}

# geographies_for_coords: call Census coordinates->geographies endpoint to get tract GEOID etc.
# Returns list(status='ok', geographies=...) or structured error/nomatch
geographies_for_coords <- function(lat, lon, benchmark = "Public_AR_Census2020", vintage = "Census2020_Census2020", timeout_seconds = 10) {
  if (missing(lat) || missing(lon) || !is.finite(lat) || !is.finite(lon)) return(list(status = "error", message = "invalid lat/lon"))
  base <- "https://geocoding.geo.census.gov/geocoder/geographies/coordinates"

  resp <- tryCatch({
    httr::GET(
      url = base,
      query = list(x = lon, y = lat, benchmark = benchmark, vintage = vintage, format = "json"),
      httr::user_agent("TommyBoy/1.0 (geographies_for_coords)"),
      httr::timeout(timeout_seconds)
    )
  }, error = function(e) {
    list(status = "error", message = paste0("HTTP error: ", e$message))
  })

  if (is.list(resp) && !inherits(resp, "response")) return(resp)
  if (httr::status_code(resp) != 200) {
    body <- tryCatch(httr::content(resp, as = "text", encoding = "UTF-8"), error = function(e) "")
    return(list(status = "error", message = paste0("HTTP status ", httr::status_code(resp), "; body: ", body)))
  }

  txt <- httr::content(resp, as = "text", encoding = "UTF-8")
  parsed <- tryCatch({ jsonlite::fromJSON(txt, simplifyVector = FALSE) }, error = function(e) NULL)
  if (is.null(parsed)) return(list(status = "error", message = "JSON parse error"))

  geogs <- parsed$result$geographies
  if (is.null(geogs) || length(geogs) == 0) return(list(status = "nomatch", message = "no geographies returned", raw = parsed$result))

  list(status = "ok", geographies = geogs)
}

# fetch_acs_population: given 11-digit tract GEOID, query Census API for total population and NAME
# Returns list(status='ok', population=..., name=...) or structured error
fetch_acs_population <- function(tract_geoid, year = 2021, key = Sys.getenv("CENSUS_API_KEY", "")) {
  if (missing(tract_geoid) || is.null(tract_geoid) || nchar(trimws(tract_geoid)) < 11) return(list(status = "error", message = "invalid tract GEOID"))

  state <- substr(tract_geoid, 1, 2)
  county <- substr(tract_geoid, 3, 5)
  tract <- substr(tract_geoid, 6, 11)
  if (nchar(tract) < 6) tract <- sprintf("%06s", tract)

  base <- sprintf("https://api.census.gov/data/%s/acs/acs5", as.character(year))

  # Try with the estimate variable (B01003_001E) which the ACS API accepts
  query <- list(get = "B01003_001E,NAME")
  query[["for"]] <- paste0("tract:", tract)
  query[["in"]] <- paste0("state:", state, "+county:", county)
  if (nzchar(key)) query$key <- key

  resp <- tryCatch({ httr::GET(base, query = query, httr::timeout(15)) }, error = function(e) list(status = "error", message = paste0("HTTP error: ", e$message)))
  if (is.list(resp) && !inherits(resp, "response")) return(resp)

  status_code <- httr::status_code(resp)
  if (status_code != 200) {
    # capture body for diagnostics
    body <- tryCatch(httr::content(resp, as = "text", encoding = "UTF-8"), error = function(e) "")
    # If variable unknown, try fallback to B01003_001 (non-E)
    if (grepl("unknown variable", body, ignore.case = TRUE)) {
      query2 <- list(get = "B01003_001,NAME")
      query2[["for"]] <- paste0("tract:", tract)
      query2[["in"]] <- paste0("state:", state, "+county:", county)
      if (nzchar(key)) query2$key <- key
      resp2 <- tryCatch({ httr::GET(base, query = query2, httr::timeout(15)) }, error = function(e) list(status = "error", message = paste0("HTTP error: ", e$message)))
      if (is.list(resp2) && !inherits(resp2, "response")) return(resp2)
      if (httr::status_code(resp2) == 200) {
        txt2 <- httr::content(resp2, as = "text", encoding = "UTF-8")
        parsed2 <- tryCatch({ jsonlite::fromJSON(txt2, simplifyVector = TRUE) }, error = function(e) NULL)
        if (!is.null(parsed2) && length(parsed2) >= 2) {
          hdr2 <- parsed2[1, ]
          vals2 <- parsed2[2, ]
          pop_idx2 <- which(hdr2 %in% c("B01003_001E", "B01003_001"))
          name_idx2 <- which(hdr2 == "NAME")
          pop2 <- if (length(pop_idx2) > 0) as.numeric(vals2[[pop_idx2]]) else NA
          name2 <- if (length(name_idx2) > 0) as.character(vals2[[name_idx2]]) else NA
          return(list(status = "ok", population = pop2, name = name2, state = state, county = county, tract = tract))
        }
      } else {
        body2 <- tryCatch(httr::content(resp2, as = "text", encoding = "UTF-8"), error = function(e) "")
        return(list(status = "error", message = paste0("Census API status: ", httr::status_code(resp2), "; body: ", body2)))
      }
    }

    # If 400 and no key, give helpful message
    if (status_code == 400 && !nzchar(key)) return(list(status = "error", message = paste0("Census API returned status 400. The Census API may require an API key for this endpoint. Set CENSUS_API_KEY in your environment. Body: ", body)))

    return(list(status = "error", message = paste0("Census API status: ", status_code, "; body: ", body)))
  }

  txt <- httr::content(resp, as = "text", encoding = "UTF-8")
  parsed <- tryCatch({ jsonlite::fromJSON(txt, simplifyVector = TRUE) }, error = function(e) NULL)
  if (is.null(parsed) || length(parsed) < 2) return(list(status = "error", message = "Census API returned no data"))

  hdr <- parsed[1, ]
  vals <- parsed[2, ]
  pop_idx <- which(hdr %in% c("B01003_001E", "B01003_001"))
  name_idx <- which(hdr == "NAME")
  pop <- if (length(pop_idx) > 0) as.numeric(vals[[pop_idx]]) else NA
  name <- if (length(name_idx) > 0) as.character(vals[[name_idx]]) else NA

  list(status = "ok", population = pop, name = name, state = state, county = county, tract = tract)
}
  • dev within app.R
    • ’s suggestion
  • 2–layers of capability
  • debugging along the way
  • heavy –lifting within source script

Week II Status:

  • accepts addresses
    • format not tested
  • locates & zooms
  • no US Census connection
  • Initial fail (app cycled without presenting visual)
    • 2 or 3 correction fails
    • did not touch after 1st week (total 1st week time = ~20 minutes)
I would like to build a Quarto dashboard, powered by 
R, that has 3 primary functions. #1 - user types an 
address, #2 - a map displays the address, and #3 -data 
is shown about the address. I do have a US Census 
API Key if we go that route for the address data, but 
I am open to alternatives. Please provide all code 
necessary to execute this dashboard 


*Out of several considered options, we chose to build via GitHub Copilot from within GitHub

Today…

typst INVOICE

& Session Info (August 11, 2026) Rendering:

R version 4.5.0 (2025-04-11 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 10 x64 (build 19045)

Matrix products: default
  LAPACK version 3.12.1

locale:
[1] LC_COLLATE=English_United States.utf8 
[2] LC_CTYPE=English_United States.utf8   
[3] LC_MONETARY=English_United States.utf8
[4] LC_NUMERIC=C                          
[5] LC_TIME=English_United States.utf8    

time zone: America/Chicago
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
 [1] compiler_4.5.0    fastmap_1.2.0     cli_3.6.5         tools_4.5.0      
 [5] htmltools_0.5.9   otel_0.2.0        rstudioapi_0.18.0 yaml_2.3.12      
 [9] rmarkdown_2.31    knitr_1.51        jsonlite_2.0.0    xfun_0.57        
[13] digest_0.6.39     rlang_1.1.7       evaluate_1.0.5   

Quarto version 1.9.37