Compare commits
20 Commits
d8f1bc3110
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a63139dee | |||
| 0b5b093d99 | |||
| 5fbff45a2e | |||
| bf5f736cf3 | |||
| d953edce54 | |||
| 29f48172fb | |||
| ef5c62d2a6 | |||
| 6514c23398 | |||
| 6614bfeb04 | |||
| 6264ebea66 | |||
| 4c6ab3d573 | |||
| 4ef1e5511a | |||
| 8e4c4ebff2 | |||
| 138051c4c4 | |||
| ce209d8898 | |||
| b115ee1158 | |||
| 3b69dd3477 | |||
| a816a570a0 | |||
| dd7566ef8e | |||
| eb18ba4115 |
@@ -17,3 +17,5 @@ data-raw/
|
||||
CLAUDE.md
|
||||
TODO.md
|
||||
README.md
|
||||
|
||||
logs/
|
||||
|
||||
@@ -7,23 +7,13 @@ on:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: self-hosted
|
||||
|
||||
steps:
|
||||
- name: Deploy via SSH
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
SERVER_IP: ${{ secrets.DEPLOY_SERVER_IP }}
|
||||
SERVER_USER: ${{ secrets.DEPLOY_SERVER_USER }}
|
||||
- name: Deploy
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
ssh-keyscan -H $SERVER_IP >> ~/.ssh/known_hosts
|
||||
ssh ${SERVER_USER}@${SERVER_IP} "
|
||||
cd /data/projects/r/stAndrews &&
|
||||
git pull origin main &&
|
||||
docker compose build &&
|
||||
docker compose up -d &&
|
||||
cd /data/projects/r/stAndrews
|
||||
git pull origin main
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
docker exec analytics-gateway caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
|
||||
"
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -27,7 +27,15 @@ data-raw/uscb/
|
||||
|
||||
# Derived data (rebuilt from data-raw via data-raw/main.R)
|
||||
data/owners.rds
|
||||
|
||||
# Downloaded HOA documents (large, not committed)
|
||||
www/docs/
|
||||
data/venice.rds
|
||||
data/venice_facts.rds
|
||||
data/beaches.rds
|
||||
.Renviron
|
||||
.aider*
|
||||
.env
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
|
||||
@@ -189,3 +189,8 @@ operation with no manual steps.
|
||||
- Geocoding used Google API via `tidygeocoder`; results cached in `data-raw/geotagged_street_addresses.rds` to avoid re-calling the API.
|
||||
- Point deduplication (multiple units at same address) was done manually in QGIS — not scripted. `owners_moved.gpkg` is the authoritative geocoded dataset.
|
||||
- `data-raw/` is gitignored except for the shapefiles in `data-raw/PlatBoundary/` and `data-raw/SarasotaCountyBoundary/` which are committed.
|
||||
|
||||
## Ops
|
||||
|
||||
- **Check latest refresh log:** `tail -50 /data/projects/r/stAndrews/logs/refresh.log`
|
||||
- Cron runs every Sunday at 11pm; logs go to `logs/refresh.log` (not `~/`)
|
||||
|
||||
187
R/app_server.R
187
R/app_server.R
@@ -4,7 +4,7 @@ create_server <- function(input, output, session) {
|
||||
# Load data
|
||||
owners <- readRDS(app_config$data_paths$owners)
|
||||
listings <- readRDS(app_config$data_paths$listings) |>
|
||||
arrange(price_per_sqft) |>
|
||||
arrange(desc(listed_date)) |>
|
||||
mutate(
|
||||
price_fmt = scales::dollar(price),
|
||||
ppsf_fmt = scales::dollar(price_per_sqft)
|
||||
@@ -16,6 +16,9 @@ create_server <- function(input, output, session) {
|
||||
ppsf_fmt = scales::dollar(price_per_sqft)
|
||||
)
|
||||
last_sale_date <- format(attr(owners, "last_sale_date"), "%Y-%m-%d")
|
||||
output$sales_date <- renderText({
|
||||
paste("Last sale date included was", last_sale_date)
|
||||
})
|
||||
sbdvn <- sf::st_read(app_config$data_paths$plats)
|
||||
beaches <- readRDS(app_config$data_paths$beaches)
|
||||
|
||||
@@ -25,63 +28,6 @@ create_server <- function(input, output, session) {
|
||||
selected = input$menu)
|
||||
})
|
||||
|
||||
# resources sub-section toggle
|
||||
res_section <- reactiveVal("beaches")
|
||||
observeEvent(input$res_beaches, { res_section("beaches") })
|
||||
observeEvent(input$res_links, { res_section("links") })
|
||||
observeEvent(input$res_restart, { res_section("restart") })
|
||||
|
||||
output$resources_content <- renderUI({
|
||||
if (res_section() == "beaches") {
|
||||
tagList(
|
||||
f7Card(
|
||||
title = "Beaches",
|
||||
divider = TRUE,
|
||||
leafletOutput("beach_map"),
|
||||
footer = p("Source: Environmental Protection Agency")
|
||||
),
|
||||
f7Block(
|
||||
h3("Helpful Links:"),
|
||||
f7List(
|
||||
inset = TRUE, dividers = TRUE, strong = TRUE, outline = FALSE,
|
||||
f7ListItem(title = "EPA Beaches", href = "https://www.epa.gov/beaches", external = TRUE),
|
||||
f7ListItem(title = "Red Tide Forecast", href = "https://habforecast.gcoos.org/", external = TRUE),
|
||||
f7ListItem(title = "Healthy Beaches Program", href = "https://fdoh.maps.arcgis.com/apps/instant/nearby/index.html?appid=7106a20597de4bff98cc5ebc7f932047&findSource=0&find=1600%2520Harbor%2520Dr%2520S%252C%2520Venice%252C%2520Florida%252C%252034285&sliderDistance=2", external = TRUE),
|
||||
f7ListItem(title = "MOTE Beach Conditions", href = "https://visitbeaches.org/beach/6/report/53033", external = TRUE),
|
||||
f7ListItem(title = "National Hurricane Center", href = "https://www.nhc.noaa.gov/", external = TRUE)
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
tagList(
|
||||
f7BlockTitle(title = "Services:", size = "medium"),
|
||||
f7Block(
|
||||
f7List(
|
||||
mode = "links", inset = TRUE, outline = TRUE, dividers = TRUE, strong = TRUE,
|
||||
f7Link(label = "City of Venice", href = "https://www.venicegov.com/"),
|
||||
f7Link(label = "Florida Power & Light", href = "https://www.fpl.com/"),
|
||||
f7Link(label = "Sarasota County", href = "https://www.scgov.net/"),
|
||||
f7Link(label = "Property Appraiser", href = "https://www.sc-pa.com/"),
|
||||
f7Link(label = "Open GIS Portal", href = "https://data-sarco.opendata.arcgis.com/"),
|
||||
f7Link(label = "Waste & Recycling", href = "https://www.venicegov.com/government/public-works/waste-and-recycling"),
|
||||
f7Link(label = "Condo Regulation", href = "https://condos.myfloridalicense.com/"),
|
||||
f7Link(label = "Property Records Search", href = "https://www.sarasotaclerk.com/records/official-records/search-land-records")
|
||||
)
|
||||
),
|
||||
f7BlockTitle(title = "Documents:", size = "medium"),
|
||||
f7Block(
|
||||
f7List(
|
||||
mode = "links", inset = TRUE, outline = TRUE, dividers = TRUE, strong = TRUE,
|
||||
f7Link(label = "St. Andrews Covenants", href = "docs/2000_01_01_st_andrews_covenants.pdf"),
|
||||
f7Link(label = "St. Andrews (unrecorded)", href = "docs/2004_06_23_sap_map.pdf"),
|
||||
f7Link(label = "Patios 2", href = "docs/1997_08_12_patios_2_plat.pdf"),
|
||||
f7Link(label = "Patios 3", href = "docs/1998_11_17_patios_3_plat.pdf"),
|
||||
f7Link(label = "Villas 2", href = "docs/1998_09_14_villas_2_plat.pdf")
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
# Initialize filteredOwners with all owners
|
||||
filteredOwners <- reactiveVal(owners)
|
||||
@@ -288,6 +234,7 @@ create_server <- function(input, output, session) {
|
||||
lng = ~longitude,
|
||||
popup = ~paste0(
|
||||
"<b>", address, "</b><br>",
|
||||
"Owner: ", owner_1, "<br>",
|
||||
"Price: ", price_fmt, "<br>",
|
||||
"Sq Ft: ", sqft, "<br>",
|
||||
"$/Sq Ft: ", ppsf_fmt
|
||||
@@ -307,6 +254,7 @@ create_server <- function(input, output, session) {
|
||||
options = list(
|
||||
pageLength = 25,
|
||||
searching = FALSE,
|
||||
scrollX = TRUE,
|
||||
dom = 't'
|
||||
)
|
||||
)
|
||||
@@ -349,6 +297,7 @@ create_server <- function(input, output, session) {
|
||||
options = list(
|
||||
pageLength = 10,
|
||||
searching = FALSE,
|
||||
scrollX = TRUE,
|
||||
dom = 't'
|
||||
)
|
||||
)
|
||||
@@ -369,125 +318,3 @@ create_server <- function(input, output, session) {
|
||||
)
|
||||
})
|
||||
}
|
||||
# Server logic for St. Andrews Shiny App
|
||||
|
||||
library(shiny)
|
||||
library(shinyMobile)
|
||||
library(sf)
|
||||
library(dplyr)
|
||||
library(leaflet)
|
||||
library(DT)
|
||||
|
||||
create_server <- function(input, output, session) {
|
||||
|
||||
# Load configuration
|
||||
source("./R/config.R", local = TRUE)
|
||||
|
||||
# Load owners data
|
||||
owners_data <- reactive({
|
||||
req(app_config$data_paths$owners)
|
||||
data <- readRDS(app_config$data_paths$owners)
|
||||
return(data)
|
||||
})
|
||||
|
||||
# Get last sale date
|
||||
last_sale_date <- reactive({
|
||||
owners <- owners_data()
|
||||
date <- attr(owners, "last_sale_date")
|
||||
if (is.null(date)) {
|
||||
return(Sys.Date())
|
||||
}
|
||||
return(date)
|
||||
})
|
||||
|
||||
# Display last sale date
|
||||
output$last_sale_date_display <- renderText({
|
||||
date <- last_sale_date()
|
||||
paste("Last sale date:", format(date, "%B %d, %Y"))
|
||||
})
|
||||
|
||||
# More server logic for the map, table, etc. would go here
|
||||
# For now, I'll add placeholders for the outputs referenced in the UI
|
||||
|
||||
# Map output
|
||||
output$map <- renderLeaflet({
|
||||
leaflet() %>%
|
||||
addTiles() %>%
|
||||
setView(lng = -82.4, lat = 27.1, zoom = 12)
|
||||
})
|
||||
|
||||
# Table output
|
||||
output$table <- renderDT({
|
||||
datatable(data.frame(Note = "Owner data would be displayed here"))
|
||||
})
|
||||
|
||||
# Listings map
|
||||
output$listings_map <- renderLeaflet({
|
||||
leaflet() %>%
|
||||
addTiles() %>%
|
||||
setView(lng = -82.4, lat = 27.1, zoom = 12)
|
||||
})
|
||||
|
||||
# Listings table
|
||||
output$listings_table <- renderDT({
|
||||
datatable(data.frame(Note = "Active listings would be displayed here"))
|
||||
})
|
||||
|
||||
# Sales map
|
||||
output$sales_map <- renderLeaflet({
|
||||
leaflet() %>%
|
||||
addTiles() %>%
|
||||
setView(lng = -82.4, lat = 27.1, zoom = 12)
|
||||
})
|
||||
|
||||
# Sales table
|
||||
output$sales_table <- renderDT({
|
||||
datatable(data.frame(Note = "Recent sales would be displayed here"))
|
||||
})
|
||||
|
||||
# Resources content
|
||||
output$resources_content <- renderUI({
|
||||
f7Card(
|
||||
title = "Community Resources",
|
||||
"Links and documents will be displayed here."
|
||||
)
|
||||
})
|
||||
|
||||
# Download handlers
|
||||
output$download_filtered <- downloadHandler(
|
||||
filename = function() {
|
||||
paste("owners-filtered-", Sys.Date(), ".csv", sep = "")
|
||||
},
|
||||
content = function(file) {
|
||||
write.csv(data.frame(Note = "Filtered owner data"), file)
|
||||
}
|
||||
)
|
||||
|
||||
output$download_all <- downloadHandler(
|
||||
filename = function() {
|
||||
paste("owners-all-", Sys.Date(), ".csv", sep = "")
|
||||
},
|
||||
content = function(file) {
|
||||
write.csv(data.frame(Note = "All owner data"), file)
|
||||
}
|
||||
)
|
||||
|
||||
# Button observers for resources tab
|
||||
observeEvent(input$res_beaches, {
|
||||
f7Dialog(
|
||||
title = "Beaches",
|
||||
text = "Beach information would be displayed here."
|
||||
)
|
||||
})
|
||||
|
||||
observeEvent(input$res_links, {
|
||||
f7Dialog(
|
||||
title = "Links",
|
||||
text = "Community links would be displayed here."
|
||||
)
|
||||
})
|
||||
|
||||
observeEvent(input$res_restart, {
|
||||
session$reload()
|
||||
})
|
||||
}
|
||||
|
||||
94
R/app_ui.R
94
R/app_ui.R
@@ -70,7 +70,7 @@ create_ui <- function() {
|
||||
f7PanelItem(
|
||||
tabName = "Sales",
|
||||
title = "Sales",
|
||||
icon = f7Icon("dollarsign_circle_fill")
|
||||
icon = f7Icon("money_dollar")
|
||||
),
|
||||
f7PanelItem(
|
||||
tabName = "Resources",
|
||||
@@ -101,6 +101,7 @@ create_ui <- function() {
|
||||
divider = "TRUE",
|
||||
tags$img(src = "images/st_andrews.jpg", width = "100%"),
|
||||
"St. Andrews Park is located in Venice, Florida. The condominiums are a mix of single-family homes, villas (2 and 4 units) and multi-unit buildings (8 units). There are 388 separate properties within 14 subdivisions. St. Andrews Park is one of many communities in the Plantation Golf and Country Club. It is should not be confused with the adjacent community St. Andrews East.",
|
||||
tags$em(textOutput("sales_date", inline = TRUE)),
|
||||
footer = tagList(
|
||||
f7Link("More Info", href = "http://www.cpmi.us/standrews-plantation/outside_home.asp")
|
||||
)
|
||||
@@ -134,7 +135,11 @@ create_ui <- function() {
|
||||
f7Select(
|
||||
inputId = "sub_name",
|
||||
label = "Select Subdivision:",
|
||||
choices = c("All", sort(sbdvn$sub_name))
|
||||
choices = c("All", "FAIRWAY GLEN", "GARDENS 1", "GARDENS 2",
|
||||
"GARDENS 3", "GARDENS 4", "PATIOS 1", "PATIOS 2",
|
||||
"PATIOS 3", "STRATFORD GLENN", "TERRACE HOMES",
|
||||
"TERRACE VILLAS", "VILLAS 1 ST", "VILLAS 2",
|
||||
"WEST LAKE GARDENS")
|
||||
),
|
||||
tags$br(),
|
||||
f7Button(
|
||||
@@ -185,7 +190,7 @@ create_ui <- function() {
|
||||
f7Tab(
|
||||
title = "Sales",
|
||||
tabName = "Sales",
|
||||
icon = f7Icon("dollarsign_circle_fill"),
|
||||
icon = f7Icon("money_dollar"),
|
||||
f7Card(
|
||||
title = "Map:",
|
||||
divider = TRUE,
|
||||
@@ -202,12 +207,85 @@ create_ui <- function() {
|
||||
title = "Resources",
|
||||
tabName = "Resources",
|
||||
icon = f7Icon("hammer_fill"),
|
||||
f7Segment(
|
||||
f7Button(inputId = "res_beaches", label = "Beaches"),
|
||||
f7Button(inputId = "res_links", label = "Links"),
|
||||
f7Button(inputId = "res_restart", label = "Restart")
|
||||
f7Accordion(
|
||||
id = "resources_accordion",
|
||||
f7AccordionItem(
|
||||
title = "Beaches",
|
||||
f7Card(
|
||||
divider = TRUE,
|
||||
leafletOutput("beach_map"),
|
||||
footer = p("Source: Environmental Protection Agency")
|
||||
),
|
||||
uiOutput("resources_content")
|
||||
f7Block(
|
||||
f7List(
|
||||
inset = TRUE, dividers = TRUE, strong = TRUE, outline = FALSE,
|
||||
f7ListItem(title = "EPA Beaches", href = "https://www.epa.gov/beaches", external = TRUE),
|
||||
f7ListItem(title = "Red Tide Forecast", href = "https://habforecast.gcoos.org/", external = TRUE),
|
||||
f7ListItem(title = "Healthy Beaches Program", href = "https://fdoh.maps.arcgis.com/apps/instant/nearby/index.html?appid=7106a20597de4bff98cc5ebc7f932047&findSource=0&find=1600%2520Harbor%2520Dr%2520S%252C%2520Venice%252C%2520Florida%252C%252034285&sliderDistance=2", external = TRUE),
|
||||
f7ListItem(title = "MOTE Beach Conditions", href = "https://visitbeaches.org/beach/6/report/53033", external = TRUE),
|
||||
f7ListItem(title = "National Hurricane Center", href = "https://www.nhc.noaa.gov/", external = TRUE)
|
||||
)
|
||||
)
|
||||
),
|
||||
f7AccordionItem(
|
||||
title = "Services",
|
||||
f7Block(
|
||||
f7List(
|
||||
inset = TRUE, dividers = TRUE, strong = TRUE, outline = FALSE,
|
||||
f7ListItem(title = "City of Venice", href = "https://www.venicegov.com/", external = TRUE),
|
||||
f7ListItem(title = "Florida Power & Light", href = "https://www.fpl.com/", external = TRUE),
|
||||
f7ListItem(title = "Sarasota County", href = "https://www.scgov.net/", external = TRUE),
|
||||
f7ListItem(title = "Property Appraiser", href = "https://www.sc-pa.com/", external = TRUE),
|
||||
f7ListItem(title = "Open GIS Portal", href = "https://data-sarco.opendata.arcgis.com/", external = TRUE),
|
||||
f7ListItem(title = "Waste & Recycling", href = "https://www.venicegov.com/government/public-works/waste-and-recycling", external = TRUE),
|
||||
f7ListItem(title = "Condo Regulation", href = "https://condos.myfloridalicense.com/", external = TRUE),
|
||||
f7ListItem(title = "Property Records Search", href = "https://www.sarasotaclerk.com/records/official-records/search-land-records", external = TRUE)
|
||||
)
|
||||
)
|
||||
),
|
||||
f7AccordionItem(
|
||||
title = "Docs",
|
||||
f7Accordion(
|
||||
id = "docs_accordion",
|
||||
f7AccordionItem(
|
||||
title = "St. Andrews Park",
|
||||
f7List(
|
||||
inset = TRUE, dividers = TRUE, strong = TRUE, outline = FALSE,
|
||||
f7ListItem(title = "Covenants", href = "docs/st_andrews/2000_01_01_st_andrews_covenants.pdf"),
|
||||
f7ListItem(title = "Map (unrecorded)", href = "docs/st_andrews/2004_06_23_sap_map.pdf")
|
||||
)
|
||||
),
|
||||
f7AccordionItem(
|
||||
title = "Patios 1",
|
||||
f7List(
|
||||
inset = TRUE, dividers = TRUE, strong = TRUE, outline = FALSE,
|
||||
f7ListItem(title = "Plat", href = "docs/patios_1/1995_12_04_patios_1_plat.pdf")
|
||||
)
|
||||
),
|
||||
f7AccordionItem(
|
||||
title = "Patios 2",
|
||||
f7List(
|
||||
inset = TRUE, dividers = TRUE, strong = TRUE, outline = FALSE,
|
||||
f7ListItem(title = "Plat", href = "docs/patios_2/1997_08_12_patios_2_plat.pdf")
|
||||
)
|
||||
),
|
||||
f7AccordionItem(
|
||||
title = "Patios 3",
|
||||
f7List(
|
||||
inset = TRUE, dividers = TRUE, strong = TRUE, outline = FALSE,
|
||||
f7ListItem(title = "Plat", href = "docs/patios_3/1998_11_17_patios_3_plat.pdf")
|
||||
)
|
||||
),
|
||||
f7AccordionItem(
|
||||
title = "Villas 2",
|
||||
f7List(
|
||||
inset = TRUE, dividers = TRUE, strong = TRUE, outline = FALSE,
|
||||
f7ListItem(title = "Plat", href = "docs/villas_2/1998_09_14_villas_2_plat.pdf")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
### end tabs----
|
||||
),
|
||||
|
||||
4
TODO.md
4
TODO.md
@@ -12,6 +12,9 @@
|
||||
- [x] Add cron job to run `update_owners.R` weekly inside the rstudio container:
|
||||
Runs every Sunday at 11pm via crontab; restarts standrews_shiny after; logs to ~/standrews_update.log
|
||||
Tested end-to-end 2026-03-09 — 388 owners written, options(timeout=300) required for 87.5 MB download
|
||||
- [x] Move cron log from ~/ to project dir (2026-04-16):
|
||||
Log now writes to /data/projects/r/stAndrews/logs/refresh.log
|
||||
logs/ added to .gitignore and .dockerignore
|
||||
|
||||
## Features
|
||||
|
||||
@@ -34,6 +37,7 @@
|
||||
|
||||
## Deployment
|
||||
|
||||
- [x] Fix SSH deploy workflow — switched to self-hosted runner (runs-on: self-hosted), eliminates SSH roundtrip
|
||||
- [x] Create Dockerfile, docker-compose.yml, .gitea/workflows/deploy.yaml
|
||||
- [x] Push to Gitea — act_runner deploys on push to main
|
||||
- [x] App live at apps.robwiederstein.org/stAndrews/
|
||||
|
||||
@@ -32,4 +32,8 @@ source("./data-raw/update_owners.R")
|
||||
cat("\n--- update_sales.R ---\n")
|
||||
source("./data-raw/update_sales.R")
|
||||
|
||||
# ── Update listings ───────────────────────────────────────────────────────────
|
||||
cat("\n--- update_listings.R ---\n")
|
||||
source("./data-raw/update_listings.R")
|
||||
|
||||
cat("\n=== Refresh complete", format(Sys.time()), "===\n")
|
||||
|
||||
@@ -51,11 +51,50 @@ 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),
|
||||
|
||||
BIN
data/listings.rds
Normal file
BIN
data/listings.rds
Normal file
Binary file not shown.
@@ -1 +1,2 @@
|
||||
"submitted_at","address","price","sqft","name","email","cell"
|
||||
"2026-03-09 21:45:25","863 Tartan",325000,1800,"Cuba Gooding Jr.","khuon68@gmail.com","2708695112"
|
||||
|
||||
|
BIN
data/sales.rds
Normal file
BIN
data/sales.rds
Normal file
Binary file not shown.
BIN
www/docs/patios_1/1995_12_04_patios_1_plat.pdf
Normal file
BIN
www/docs/patios_1/1995_12_04_patios_1_plat.pdf
Normal file
Binary file not shown.
Reference in New Issue
Block a user