# update_listings.R # Weekly update: pull active listings from RentCast within 1 mile of # St. Andrews center, clip to plat boundary, save data/listings.rds. # Input: .Renviron (RENTCAST_API_KEY) # data/plats/plats.shp # Output: data/listings.rds library(httr2) library(jsonlite) library(sf) library(dplyr) # ── API key ─────────────────────────────────────────────────────────────────── api_key <- Sys.getenv("RENTCAST_API_KEY") if (identical(api_key, "")) stop("Missing RENTCAST_API_KEY in .Renviron") # ── Pull listings ───────────────────────────────────────────────────────────── resp <- request("https://api.rentcast.io/v1/listings/sale") |> req_headers( "Accept" = "application/json", "X-Api-Key" = api_key ) |> req_url_query( latitude = 27.076199, longitude = -82.362253, radius = 1, status = "Active", limit = 500 ) |> req_retry(max_tries = 3) |> req_perform() if (resp_status(resp) >= 400) stop("RentCast API error: HTTP ", resp_status(resp), "\n", resp_body_string(resp)) listings_raw <- jsonlite::fromJSON(resp_body_string(resp), flatten = TRUE) cat("Fetched:", nrow(listings_raw), "listings\n") # ── Clip to St. Andrews plat boundary ──────────────────────────────────────── plats <- sf::st_read("./data/plats/plats.shp", quiet = TRUE) |> sf::st_union() listings_sf <- sf::st_as_sf( listings_raw, coords = c("longitude", "latitude"), crs = 4326, remove = FALSE ) in_plat <- lengths(sf::st_within(listings_sf, plats)) > 0 listings <- listings_raw[in_plat, ] cat("After plat clip:", nrow(listings), "listings\n") # ── Override RentCast coordinates with building footprint geometry ──────────── # RentCast geocoding is approximate. Our owners data uses building centroids # from Sarasota County GIS footprints — far more accurate. Match on house # number + street name and substitute when found. owners_sf <- readRDS("./data/owners.rds") # Extract house number from owners location field (e.g. "878 CHALMERS DR, Venice FL") owners_coords <- owners_sf |> mutate( house_num = trimws(sub("^(\\d+).*", "\\1", location)), street_raw = trimws(sub("^\\d+\\s+(.*),.*$", "\\1", location)), match_key = paste(house_num, toupper(street_raw)) ) |> select(match_key, owner_1, geom) |> distinct(match_key, .keep_all = TRUE) # Extract house number + street from RentCast address # e.g. "878 Chalmers Dr, Unit 878, Venice, FL 34293" -> "878 CHALMERS DR" listings <- listings |> mutate( house_num = sub("^(\\d+)\\s.*", "\\1", addressLine1), street_raw = gsub("[^A-Za-z ]", "", sub("^\\d+\\s+(\\S+\\s+\\S+).*", "\\1", addressLine1)), match_key = paste(house_num, toupper(trimws(street_raw))) ) matched <- merge(listings, owners_coords, by = "match_key", all.x = TRUE) # For matched rows, replace RentCast lat/lng with footprint centroid coords has_geom <- !is.na(matched$geom) if (any(has_geom)) { coords <- sf::st_coordinates(sf::st_as_sf(matched[has_geom, ], sf_column_name = "geom")) matched$longitude[has_geom] <- coords[, "X"] matched$latitude[has_geom] <- coords[, "Y"] cat("Coordinates corrected from building footprints:", sum(has_geom), "listing(s)\n") } listings <- matched # ── Select and clean columns ────────────────────────────────────────────────── listings <- listings |> transmute( listed_date = as.Date(listedDate), address = formattedAddress, owner_1, sqft = as.numeric(squareFootage), price = as.numeric(price), price_per_sqft = round(price / sqft, 0), latitude, longitude ) |> arrange(desc(listed_date)) cat("Listings saved:", nrow(listings), "\n") saveRDS(listings, "./data/listings.rds")