openvibers/openvibe.rent Commits

HUD's Fair Market Rents on every city page and in the fair check, from OpenVibe.Sources (#84)

OP OpenVibers committed 21 days ago · 19a309fe2a · 13 files, +390 −6

Changes

ddiff --git a/README.md b/README.md
iindex 9737efd..e5c2e83 100644
---- a/README.md
++++ b/README.md
@@@ -19,6 +19,12 @@ margin is typical; far below warns of scams). A listing posted here in a listed
are in the sitemap and llms.txt, and each city page is an OpenVibe.Search document (type `city`, id `<state>-<slug>`,
sent by the search sweep; contracts 0.197.0). The data is `server/data/cities.json`, built by `node tools/build-cities.js <dir>` from the
Census Bureau's keyless ACS 5-year table files and gazetteer (see the script for the URLs); nothing is fetched at run time.
++Beside the Census medians, each city page shows **HUD's Fair Market Rents** for the city's FMR area (a studio to four
++bedrooms, rent plus essential utilities, what Housing Choice Vouchers are measured against), and the fair check names
++the one for that size. Rent reads them from OpenVibe.Sources (`hud-fair-market-rents`, category `housing`,
++`sources.item.read`), never from HUD ([server/housing/fair-market-rents.js](server/housing/fair-market-rents.js)); which
++area a city lies in is `server/data/city-fmr-areas.json`, built once by `node tools/build-fmr-areas.js` from each city's
++Census point on HUD's own FMR layer.
**OpenVibe.Homes** (since 2026-10-15, https://openvibe.homes): the buying side, served by this process for its own host
(`server/homes`, `deploy/nginx/openvibe.homes.conf`).
@@@ -113,6 +119,10 @@ Rent's own data is its PostgreSQL database ([server/db.js](server/db.js)); the `
- **`area_price_index`** — the FHFA House Price Index rows behind "Home prices around here": the last six years of
traditional, all-transactions, quarterly State and MSA index values, replaced whole on each refresh
([migrations/0006_area_price_index.sql](migrations/0006_area_price_index.sql)).
++- **`rent_fmr_areas`**, **`rent_ingest_cursor`** (0010) — HUD's Fair Market Rents as OpenVibe.Sources holds them, one row
++ per FMR area (HUD's code and name, the five monthly rents), and the cursor in Sources' change feed; an area Sources
++ removes is deleted. Public domain; nothing about a person
++ ([migrations/0010_fair_market_rents.sql](migrations/0010_fair_market_rents.sql)).
- **`telemetry_samples`** — request telemetry (`platform.telemetry-sample@1`): each request by route template, method
and status class, never the raw URL. Once a minute one `http.request` sample per key, plus the active-requests,
p95 latency and event-loop-lag gauges, is written; rows over 14 days old are deleted daily. `/shared` files,
@@@ -228,6 +238,11 @@ estimates, table B25031 (median gross rent by bedrooms) and B01003 (population),
gazetteer: public domain, cited on every page with the years. A median the Census does not publish is shown as "not
published", and its top code is shown as "$3,500 or more", never as a number.
++**Fair Market Rents** come from **HUD** through **OpenVibe.Sources**, which reads HUD's Fair Market Rents layer once a
++week under its robots and terms checks: public domain (US federal work), cited under every city table with a link to
++https://www.huduser.gov/portal/datasets/fmr.html. Rent itself never calls HUD for them; the one-time area build
++(`tools/build-fmr-areas.js`) is a developer step, about 900 point queries at most one a second.
++
**Affordable housing near you** has three verified, open sources ([server/housing/upstream.js](server/housing/upstream.js)
and [server/housing/hpi.js](server/housing/hpi.js)):
ddiff --git a/migrations/0010_fair_market_rents.sql b/migrations/0010_fair_market_rents.sql
nnew file mode 100644
iindex 0000000..b10f8a8
---- /dev/null
++++ b/migrations/0010_fair_market_rents.sql
@@@ -0,0 +1,26 @@
++-- phase: expand
++-- OpenVibe.Rent: HUD's Fair Market Rents (server/housing/fair-market-rents.js), as OpenVibe.Sources reads them from HUD
++-- every week (source hud-fair-market-rents, category housing). Rent never calls HUD for them: it follows Sources'
++-- change feed from its cursor here (openvibe-publishing/ingest, prefix rent) and keeps one row per FMR area. A city
++-- page finds its area through server/data/city-fmr-areas.json. Public domain data; nothing here belongs to a person.
++
++-- The DDL is openvibe-publishing lib/ingest.js schema('rent').
++CREATE TABLE IF NOT EXISTS rent_ingest_cursor (
++ name text COLLATE "C" NOT NULL,
++ cursor bigint NOT NULL,
++ updated_at bigint NOT NULL,
++ PRIMARY KEY (name)
++);
++
++CREATE TABLE rent_fmr_areas (
++ code text COLLATE "C" PRIMARY KEY, -- HUD's FMR_CODE (METRO35620MM5600), Sources' identity
++ item_id text COLLATE "C" NOT NULL UNIQUE, -- the Sources item
++ revision integer NOT NULL, -- the Sources item's revision this row holds
++ name text NOT NULL, -- FMR_AREANAME, as HUD names it
++ fmr_0br integer, -- dollars a month, rent plus essential utilities
++ fmr_1br integer,
++ fmr_2br integer,
++ fmr_3br integer,
++ fmr_4br integer,
++ retrieved_at text COLLATE "C" NOT NULL -- when Sources last read it, ISO 8601 UTC
++);
ddiff --git a/public/css/cities.css b/public/css/cities.css
iindex 55e7b58..5637f49 100644
---- a/public/css/cities.css
++++ b/public/css/cities.css
@@@ -18,6 +18,8 @@
.rent-sizes { width: 100%; border-collapse: collapse; }
.rent-sizes th, .rent-sizes td { padding: 7px 0; border-bottom: 1px solid var(--border, #1f2d47); text-align: left; }
.rent-sizes td { text-align: right; font-variant-numeric: tabular-nums; font-weight: 650; }
++.rent-sizes-fmr thead th { font-size: .78rem; font-weight: 600; color: var(--text-secondary, #96a7c2); text-align: right; padding-top: 0; }
++.rent-sizes-fmr td + td { padding-left: 12px; }
.rent-sizes tr.rent-all th, .rent-sizes tr.rent-all td { border-bottom: 0; color: var(--text-secondary, #96a7c2); font-weight: 500; }
.fair-card .search-form { margin: 8px 0 0; padding: 0; border: 0; background: none; }
.fair-form .fair-row { display: grid; gap: 12px; grid-template-columns: 1fr 1fr; }
ddiff --git a/server/app.js b/server/app.js
iindex 0a631d4..56edcd1 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -82,6 +82,9 @@ async function createApp(opts = {}) {
// Drop expired geocode/area answers now and hourly; an expired row is ignored anyway, this only keeps the table
// small. The interval is unref'd and kept on ctx so the entry point clears it on shutdown (server/index.js).
ctx.housingPruneTimer = housingStore.startCachePrune(s, { log });
++ // HUD's Fair Market Rents from OpenVibe.Sources (server/housing/fair-market-rents.js), for the city pages; a test
++ // passes a stand-in source through opts.fmrSource.
++ ctx.fmr = require('./housing/fair-market-rents').createFairMarketRents({ config, s, fetchImpl, log, source: opts.fmrSource || null });
// Account export and deletion (ADR-033, ./identity/account-data.js): the three tables that hold a person's rows.
// The sender posts to Network's internal export/deletion routes with this service's own client-credentials token;
ddiff --git a/server/config.js b/server/config.js
iindex f605f97..4f8a7a6 100644
---- a/server/config.js
++++ b/server/config.js
@@@ -93,6 +93,9 @@ function load(env = process.env) {
// OpenVibe.AI → the listing screen (server/listings/screen.js): each posted or edited listing is read by the
// rent.screen_listing workflow for the signs of a rental scam, for staff. Unset: off (nothing is screened).
aiUrl: trim(env.OV_AI_INTERNAL_URL || ''),
++ // OpenVibe.Sources (server/housing/fair-market-rents.js): HUD's Fair Market Rents, read from its housing category
++ // with this service's token (sources.item.read). Rent never asks HUD for them itself. Empty: off.
++ sources: { internalUrl: trim(env.OV_SOURCES_INTERNAL_URL == null ? 'http://127.0.0.1:4720' : env.OV_SOURCES_INTERNAL_URL) },
events: {
secrets: String(env.RENT_EVENTS_SECRET || '').split(',').map((x) => x.trim()).filter(Boolean),
url: trim(env.RENT_EVENTS_URL || env.EVENTS_URL || ''),
ddiff --git a/server/data/city-fmr-areas.json b/server/data/city-fmr-areas.json
nnew file mode 100644
iindex 0000000..d0964d0
---- /dev/null
++++ b/server/data/city-fmr-areas.json
@@@ -0,0 +1 @@
++{"source":{"name":"U.S. Department of Housing and Urban Development, Fair Market Rents feature layer","url":"https://hudgis-hud.opendata.arcgis.com/datasets/fair-market-rents","licence":"public domain (US federal government work)","built":"2026-10-22","note":"Each city's Census internal point, located in HUD's FMR areas. The rents come from OpenVibe.Sources (hud-fair-market-rents), not from this file."},"areas":{"1077580":"METRO37980M37980","1150000":"METRO47900M47900","1200410":"METRO36740M36740","1201700":"METRO36740M36740","1207300":"METRO33100MM8960","1207525":"METRO15980M15980","1207875":"METRO33100MM8960","1207950":"METRO35840M35840","1208150":"METRO45300M45300","1210275":"METRO15980M15980","1212875":"METRO45300M45300","1213275":"METRO33100MM2680","1214400":"METRO33100MM2680","1214895":"METRO33100MM5000","1216475":"METRO33100MM2680","1216525":"METRO19660M19660","1216725":"METRO33100MM2680","1217100":"METRO33100MM8960","1217200":"METRO19660M19660","1217935":"METRO33100MM5000","1224000":"METRO33100MM2680","1224125":"METRO15980M15980","1224562":"METRO33100MM5000","1224581":"METRO36740M36740","1225175":"METRO23540M23540","1230000":"METRO33100MM5000","1232000":"METRO33100MM2680","1232275":"METRO33100MM5000","1232610":"METRO36740M36740","1235000":"METRO27260M27260","1235875":"METRO33100MM8960","1236062":"METRO33100MM5000","1236100":"METRO33100MM5000","1236950":"METRO36740M36740","1238250":"METRO29460M29460","1239425":"METRO45300M45300","1239550":"METRO33100MM2680","1239925":"METRO15980M15980","1243125":"METRO33100MM2680","1243975":"METRO37340M37340","1245000":"METRO33100MM5000","1245025":"METRO33100MM5000","1245060":"METRO33100MM5000","1245975":"METRO33100MM2680","1249450":"METRO33100MM5000","1249675":"METRO35840M35840","1250750":"METRO36100M36100","1253000":"METRO36740M36740","1254000":"METRO37340M37340","1254075":"METRO33100MM8960","1254200":"METRO19660M37380","1254350":"METRO45300M45300","1255775":"METRO33100MM2680","1255925":"METRO37860M37860","1256825":"METRO36740M36740","1256975":"METRO45300M45300","1257425":"METRO33100MM2680","1257900":"METRO29460M29460","1258050":"METRO33100MM2680","1258350":"METRO39460M39460","1258575":"METRO19660M19660","1258715":"METRO38940M38940","1260950":"METRO45300M45300","1262625":"METRO36740M36740","1263000":"METRO45300M45300","1263650":"METRO36740M36740","1264175":"METRO35840M35840","1268350":"METRO45300M45300","1269700":"METRO33100MM2680","1270600":"METRO45220M45220","1270675":"METRO33100MM2680","1271000":"METRO45300M45300","1271569":"METRO33100MM5000","1271625":"METRO48680M48680","1272145":"METRO45300M45300","1275812":"METRO33100MM8960","1275875":"METRO45300M45300","1276075":"METRO33100MM5000","1276582":"METRO33100MM2680","1276600":"METRO33100MM8960","1278275":"METRO29460M29460","1301052":"METRO10500M10500","1301696":"METRO12060M12060","1303440":"METRO12020M12020","1304000":"METRO12060M12060","1304204":"METRO12260M12260","1310944":"METRO12060M12060","1319000":"METRO17980M17980","1324768":"METRO12060M12060","1342425":"METRO12060M12060","1348288":"METRO12060M12060","1349008":"METRO31420M31420","1349756":"METRO12060M12060","1367284":"METRO12060M12060","1368516":"METRO12060M12060","1369000":"METRO42340M42340","1371492":"METRO12060M12060","1372122":"METRO12060M12060","1373784":"METRO12060M12060","1378800":"METRO46660M46660","1380508":"METRO47580M47580","1506290":"METRO46520M46520","1514650":"NCNTY15001N15001","1571550":"METRO46520M46520","1608830":"METRO14260M14260","1612250":"METRO14260M14260","1616750":"METRO17660M17660","1639700":"METRO26820M26820","1652120":"METRO14260M14260","1656260":"METRO14260M14260","1664090":"METRO38540M38540","1682810":"METRO46300N16083","1702154":"METRO16980M16980","1703012":"METRO16980M16980","1705573":"METRO16980M16980","1706613":"METRO14010M14010","1707133":"METRO16980M16980","1712385":"METRO16580M16580","1714000":"METRO16980M16980","1714351":"METRO16980M16980","1718823":"METRO19500M19500","1719642":"METRO16980M16980","1720591":"METRO16980M16980","1723074":"METRO16980M16980","1724582":"METRO16980M16980","1735411":"METRO16980M16980","1738570":"METRO16980M16980","1751089":"METRO16980M16980","1751622":"METRO16980M16980","1753234":"METRO14010M14010","1754820":"METRO16980M16980","1754885":"METRO16980M16980","1756640":"METRO16980M16980","1757225":"METRO16980M16980","1759000":"METRO37900M37900","1765000":"METRO40420M40420","1768003":"METRO16980M16980","1770122":"METRO16980M16980","1772000":"METRO44100M44100","1775484":"METRO16980M16980","1779293":"METRO16980M16980","1781048":"METRO16980M16980","1801468":"METRO26900M11300","1805860":"METRO14020MM1020","1810342":"METRO26900M26900","1814734":"METRO18020M18020","1820728":"METRO21140M21140","1822000":"METRO21780M21780","1823278":"METRO26900M26900","1825000":"METRO23060M23060","1827000":"METRO16980MM2960","1829898":"METRO26900M26900","1831000":"METRO16980MM2960","1836003":"METRO26900M26900","1838358":"METRO31140M31140","1840392":"METRO29020M29020","1840788":"METRO29200M29140","1849932":"METRO43780M43780","1851876":"METRO34620M34620","1854180":"METRO26900M26900","1871000":"METRO43780M43780","1875428":"METRO45460M45460","1882700":"METRO26900M26900","1901855":"METRO11180M11180","1902305":"METRO19780M19780","1912000":"METRO16300M16300","1916860":"METRO36540M36540","1919000":"METRO19340M19340","1921000":"METRO19780M19780","1922395":"METRO20220M20220","1938595":"METRO26980MM3500","1973335":"METRO43580M43580","1982425":"METRO47940M47940","1983910":"METRO19780M19780","2036000":"METRO28140M28140","2038900":"METRO29940M29940","2039350":"METRO28140M28140","2044250":"METRO31740M31740","2052575":"METRO28140M28140","2053775":"METRO28140M28140","2064500":"METRO28140M28140","2071000":"METRO45820M45820","2079000":"METRO48620M48620","2108902":"METRO14540M14540","2146027":"METRO30460M30460","2148006":"METRO31140M31140","2158620":"METRO36980M36980","2205000":"METRO12940M12940","2208920":"METRO43340M43340","2239475":"METRO35380M35380","2240735":"METRO29180M29180","2241155":"METRO29340M29340","2250115":"METRO35380M35380","2255000":"METRO35380M35380","2270000":"METRO43340M43340","2360545":"METRO38860MM6400","2402825":"METRO47900M47900","2404000":"METRO12580M12580","2405950":"METRO12580M12580","2407125":"METRO47900M47900","2408775":"METRO47900M47900","2419125":"METRO12580M12580","2423975":"METRO12580M12580","2426000":"METRO12580M12580","2430325":"METRO47900M47900","2431175":"METRO47900M47900","2432025":"METRO47900M47900","2432650":"METRO12580M12580","2467675":"METRO47900M47900","2471150":"METRO12580M12580","2472450":"METRO47900M47900","2478425":"METRO12580M12580","2481175":"METRO47900M47900","2483775":"METRO47900M47900","2507000":"METRO14460MM1120","2509000":"METRO14460MM1200","2509210":"METRO14460MM1120","2511000":"METRO14460MM1120","2513660":"METRO44140M44140","2521990":"METRO14460MM1120","2523000":"METRO39300M39300","2524960":"METRO14460MM1120","2529405":"METRO14460MM4160","2534550":"METRO14460MM4160","2537000":"METRO14460MM4560","2537490":"METRO14460MM1120","2537875":"METRO14460MM1120","2539835":"METRO14460MM1120","2540675":"METRO14460MM4160","2545000":"METRO39300MM5400","2545560":"METRO14460MM1120","2552490":"METRO14460MM1120","2555745":"METRO14460MM1120","2556585":"METRO14460MM1120","2562535":"METRO14460MM1120","2567000":"METRO44140M44140","2569170":"METRO39300MM1120","2572600":"METRO14460MM1120","2578972":"METRO14460MM1120","2582000":"METRO49340M49340","2603000":"METRO11460M11460","2605920":"METRO12980M12980","2621000":"METRO19820M19820","2621020":"METRO19820M19820","2622000":"METRO19820M19820","2627440":"METRO19820M19820","2629000":"METRO22420M22420","2634000":"METRO24340M24340","2642160":"METRO28020M28020","2642820":"METRO24340M24340","2646000":"METRO29620M29620","2649000":"METRO19820M19820","2659440":"METRO19820M19820","2665440":"METRO19820M19820","2669035":"METRO19820M19820","2670040":"METRO19820M19820","2670760":"METRO19820M19820","2674900":"METRO19820M19820","2676460":"METRO19820M19820","2679000":"METRO19820M19820","2680700":"METRO19820M19820","2684000":"METRO19820M19820","2686000":"METRO19820M19820","2688940":"METRO24340M24340","2701900":"METRO33460M33460","2706382":"METRO33460M33460","2706616":"METRO33460M33460","2707966":"METRO33460M33460","2708794":"METRO33460M33460","2713114":"METRO33460M33460","2717000":"METRO20260M20260","2717288":"METRO33460M33460","2718116":"METRO33460M33460","2718188":"METRO33460M33460","2735180":"METRO33460M33460","2740166":"METRO33460M33460","2743000":"METRO33460M33460","2743252":"METRO33460M33460","2751730":"METRO33460M33460","2754880":"METRO40340M40340","2756896":"METRO41060M41060","2758000":"METRO33460M33460","2771428":"METRO33460M33460","2829700":"METRO25060M25060","2836000":"METRO27140M27140","2869280":"METRO32820M32820","2906652":"METRO28140M28140","2915670":"METRO17860M17860","2924778":"METRO41180M41180","2935000":"METRO28140M28140","2937592":"METRO27900M27900","2938000":"METRO28140M28140","2941348":"METRO28140M28140","2954074":"METRO41180M41180","2964082":"METRO41180M41180","2964550":"METRO41140M41140","2965000":"METRO41180M41180","2965126":"METRO41180M41180","2970000":"METRO44180M44180","3006550":"METRO13740M13740","3008950":"METRO14580M14580","3032800":"METRO24500M24500","3050200":"METRO33540M33540","3103950":"METRO36540M36540","3119595":"METRO24260N31079","3128000":"METRO30700M30700","3137000":"METRO36540M36540","3209700":"METRO16180M16180","3223770":"METRO29820M29820","3231900":"METRO29820M29820","3240000":"METRO29820M29820","3251800":"METRO29820M29820","3254600":"METRO29820M29820","3260600":"METRO39900M39900","3268400":"METRO39900M39900","3268585":"METRO29820M29820","3271400":"METRO29820M29820","3345140":"METRO31700MM4760","3350260":"METRO31700MM5350","3403580":"METRO35620MM3640","3410000":"METRO37980M37980","3413690":"METRO35620MM0875","3419390":"METRO35620MM5640","3421000":"METRO35620MM5640","3432250":"METRO35620MM3640","3436000":"METRO35620MM3640","3438580":"METRO35620MM5190","3451000":"METRO35620MM5640","3451210":"METRO35620MM5015","3456550":"METRO35620MM0875","3457000":"METRO35620MM0875","3458200":"METRO35620MM5015","3459190":"METRO35620MM5640","3473110":"METRO35620MM5190","3474000":"METRO45940M45940","3474630":"METRO35620MM3640","3476070":"METRO47220M47220","3479610":"METRO35620MM3640","3502000":"METRO10740M10740","3539380":"METRO29740M29740","3563460":"METRO10740M10740","3570500":"METRO42140M42140","3601000":"METRO10580M10580","3611000":"METRO15380M15380","3615000":"METRO15380M15380","3633139":"METRO35620MM5380","3637737":"METRO40380M40380","3642081":"METRO35620MM5380","3649121":"METRO35620MM5600","3650617":"METRO35620MM5600","3651000":"METRO35620MM5600","3663000":"METRO40380M40380","3665508":"METRO10580M10580","3673000":"METRO45060M45060","3675021":"METRO15380M15380","3675484":"METRO10580M10580","3676540":"METRO46540M46540","3681677":"METRO35620MM5600","3684000":"METRO35620MM5600","3701520":"METRO39580M39580","3702140":"METRO11700M11700","3709060":"METRO15500M15500","3710740":"METRO39580M39580","3711800":"METRO20500M20500","3712000":"METRO16740M16740","3714100":"METRO16740M16740","3719000":"METRO20500M20500","3722920":"METRO22180M22180","3725580":"METRO16740M16740","3728000":"METRO24660M24660","3728080":"METRO24780M24780","3731400":"METRO24660M24660","3733120":"METRO16740M16740","3734200":"METRO27340M27340","3735200":"METRO16740M16740","3744220":"METRO16740N37097","3755000":"METRO39580M39580","3757500":"METRO40580M40580","3770540":"METRO39580M39580","3774440":"METRO48900M48900","3775000":"METRO49180M49180","3807200":"METRO13900M13900","3825700":"METRO22020M22020","3832060":"METRO24220M24220","3901000":"METRO10420M10420","3912000":"METRO15940M15940","3915000":"METRO17140M17140","3916000":"METRO17410N17460","3918000":"METRO18140M18140","3919778":"METRO10420M10420","3921000":"METRO19430M19430","3925256":"METRO17410N17460","3933012":"METRO17140M17140","3940040":"METRO19430M19430","3941664":"METRO17410N17460","3944856":"METRO17410N17460","3949840":"METRO17140M17140","3954040":"METRO18140M18140","3961000":"METRO17410N17460","3974118":"METRO44220M44220","3977000":"METRO45780M45780","3988000":"METRO49660M49660","4009050":"METRO46140M46140","4023200":"METRO36420M36420","4023950":"METRO21420M21420","4041850":"METRO30020M30020","4048350":"METRO36420M36420","4049200":"METRO36420M36420","4052500":"METRO36420M36420","4055000":"METRO36420M36420","4075000":"METRO46140M46140","4101000":"METRO10540M10540","4101650":"METRO38900M38900","4105350":"METRO38900M38900","4105800":"METRO13460M13460","4115800":"METRO18700M18700","4123850":"METRO21660M21660","4131250":"METRO38900M38900","4134100":"METRO38900M38900","4147000":"METRO32780M32780","4159000":"METRO38900M38900","4164900":"METRO41420M41420","4169600":"METRO21660M21660","4173650":"METRO38900M38900","4202000":"METRO10900M10900","4206088":"METRO10900M10900","4224000":"METRO21500M21500","4232800":"METRO25420M25420","4241216":"METRO29540M29540","4242928":"METRO37980M37980","4260000":"METRO37980M37980","4261000":"METRO38300M38300","4263624":"METRO39740M39740","4269000":"METRO42540M42540","4419180":"METRO39300M39300","4454640":"METRO39300M39300","4459000":"METRO39300M39300","4474300":"METRO39300M39300","4513330":"METRO16700M16700","4516000":"METRO17900M17900","4530850":"METRO24860M24860","4548535":"METRO16700M16700","4550875":"METRO16700M16700","4561405":"METRO16740M16740","4570270":"METRO16700M16700","4652980":"METRO39660M39660","4659020":"METRO43620M43620","4703440":"METRO32820M32820","4714000":"METRO16860M16860","4715160":"METRO17300M17300","4716420":"METRO32820M32820","4727740":"METRO34980M34980","4733280":"METRO34980M34980","4737640":"METRO27180M27180","4738320":"METRO27740M27740","4739560":"METRO28700M28700","4740000":"METRO28940M28940","4748000":"METRO32820M32820","4751560":"METRO34980M34980","4752006":"METRO34980M34980","4769420":"METRO34980M34980","4770580":"METRO34980N47119","4801000":"METRO10180M10180","4801924":"METRO19100M19100","4803000":"METRO11100M11100","4804000":"METRO19100MM2800","4804462":"METRO26420M26420","4805000":"METRO12420M12420","4806128":"METRO26420M26420","4807000":"METRO13140M13140","4810768":"METRO15180M15180","4810912":"METRO17780M17780","4811428":"METRO19100MM2800","4813024":"METRO19100M19100","4813552":"METRO12420M12420","4815976":"METRO17780M17780","4816432":"METRO26420M26420","4817000":"METRO18580M18580","4819000":"METRO19100M19100","4819972":"METRO19100M19100","4820092":"METRO19100M19100","4822660":"METRO32580M32580","4824000":"METRO21340M21340","4824768":"METRO19100MM2800","4826232":"METRO19100M19100","4827000":"METRO19100MM2800","4827684":"METRO19100M19100","4828068":"METRO26420M26420","4829000":"METRO19100M19100","4829336":"METRO12420M12420","4830464":"METRO19100M19100","4830644":"METRO19100MM2800","4832372":"METRO15180M15180","4835000":"METRO26420M26420","4837000":"METRO19100M19100","4839148":"METRO28660M28660","4839952":"METRO12420M12420","4841464":"METRO29700M29700","4841980":"METRO26420M26420","4842016":"METRO12420M12420","4842508":"METRO19100M19100","4843012":"METRO19100M19100","4843888":"METRO30980M30980","4845000":"METRO31180M31180","4845384":"METRO32580M32580","4845744":"METRO19100M19100","4846452":"METRO19100MM2800","4847892":"METRO19100M19100","4848072":"METRO33260M33260","4848768":"METRO32580M32580","4848804":"METRO26420M26420","4850820":"METRO41700M41700","4852356":"METRO19100MM2800","4853388":"METRO36220M36220","4856000":"METRO26420M26420","4856348":"METRO26420MM1145","4857176":"METRO12420M12420","4857200":"METRO32580M32580","4858016":"METRO19100M19100","4858820":"METRO13140M13140","4861796":"METRO19100M19100","4862828":"METRO19100M19100","4863500":"METRO12420M12420","4863572":"METRO19100M19100","4864472":"METRO41660M41660","4865000":"METRO41700M41700","4865600":"METRO12420M12420","4869596":"METRO26420M26420","4870808":"METRO26420M26420","4872176":"METRO28660M28660","4872392":"METRO26420M26420","4872656":"METRO26420M26420","4874144":"METRO46340M46340","4875428":"METRO47020M47020","4876000":"METRO47380M47380","4879000":"METRO48660M48660","4880356":"METRO19100M19100","4920120":"METRO41620M41620","4920810":"METRO39340M39340","4934970":"METRO41620M41620","4943660":"METRO36260M36260","4944320":"METRO39340M39340","4945860":"METRO30860M30860","4950150":"METRO41620M41620","4953230":"METRO41620M41620","4955980":"METRO36260M36260","4957300":"METRO39340M39340","4962470":"METRO39340M39340","4965330":"METRO41100M41100","4967000":"METRO41620M41620","4967440":"METRO41620M41620","4970850":"METRO41620M41620","4975360":"METRO41620M41620","4982950":"METRO41620M41620","4983470":"METRO41620M41620","5010675":"METRO15540M15540","5101000":"METRO47900M47900","5103000":"METRO47900M47900","5114440":"METRO47900M47900","5116000":"METRO47260M47260","5121088":"METRO47900M47900","5135000":"METRO47260M47260","5135624":"METRO25500M25500","5147672":"METRO31340M31340","5156000":"METRO47260M47260","5157000":"METRO47260M47260","5164000":"METRO47260M47260","5166672":"METRO47900M47900","5167000":"METRO40060M40060","5168000":"METRO40220M40220","5176432":"METRO47260M47260","5182000":"METRO47260M47260","5303180":"METRO42660MM7600","5305210":"METRO42660MM7600","5305280":"METRO13380M13380","5308850":"METRO42660MM7600","5322640":"METRO42660MM7600","5323515":"METRO42660MM7600","5335275":"METRO28420M28420","5335415":"METRO42660MM7600","5335940":"METRO42660MM7600","5336745":"METRO36500M36500","5338038":"METRO42660MM8200","5343955":"METRO42660MM7600","5351300":"METRO36500M36500","5353545":"METRO28420M28420","5357535":"METRO42660MM7600","5357745":"METRO42660MM7600","5358235":"METRO28420M28420","5361115":"METRO42660MM7600","5363000":"METRO42660MM7600","5363960":"METRO42660MM7600","5365922":"METRO42660MM8200","5367000":"METRO44060M44060","5367167":"METRO44060M44060","5370000":"METRO42660MM8200","5374060":"METRO38900M38900","5380010":"METRO49420M49420","5414600":"METRO16620M16620","5502375":"METRO11540M11540","5522300":"METRO20740M20740","5531000":"METRO24580M24580","5537825":"METRO27500M27500","5539225":"METRO28450M28450","5540775":"METRO29100M29100","5548000":"METRO31540M31540","5553000":"METRO33340M33340","5560500":"METRO36780M36780","5566000":"METRO39540M39540","5584250":"METRO33340M33340","5585300":"METRO33340M33340","5613150":"METRO16220M16220","5613900":"METRO16940M16940","7206593":"METRO41980MM7440","7210334":"METRO41980MM1310","7214290":"METRO41980MM7440","7232522":"METRO41980MM7440","7252431":"METRO32420M32420","7263820":"METRO38660M38660","7276770":"METRO41980MM7440","0644000":"METRO31080MM4480","0455000":"METRO38060M38060","0666000":"METRO41740M41740","0668000":"METRO41940M41940","0667000":"METRO41860MM7360","0820000":"METRO19740M19740","0477000":"METRO46060M46060","0627000":"METRO23420M23420","0664000":"METRO40900M40900","0446000":"METRO38060M38060","0816000":"METRO17820M17820","0643000":"METRO31080MM4480","0653000":"METRO41860MM5775","0603526":"METRO12540M12540","0804000":"METRO19740M19740","0602000":"METRO31080MM5945","0675000":"METRO44700M44700","0662000":"METRO40140M40140","0669000":"METRO31080MM5945","0636770":"METRO31080MM5945","0203000":"METRO11260M11260","0427400":"METRO38060M38060","0412000":"METRO38060M38060","0613392":"METRO41740M41740","0427820":"METRO38060M38060","0465000":"METRO38060M38060","0669088":"METRO31080MM4480","0626000":"METRO41860MM5775","0137000":"METRO26620M26620","0665000":"METRO40140M40140","0648354":"METRO33700M33700","0624680":"METRO40140M40140","0649270":"METRO40140M40140","0541000":"METRO30780M30780","0150000":"METRO33660M33660","0654652":"METRO37100M37100","0107000":"METRO13820M13820","0151000":"METRO33860M33860","0454050":"METRO38060M38060","0636000":"METRO31080MM5945","0630000":"METRO31080MM4480","0473000":"METRO38060M38060","0653896":"METRO40140M40140","0622020":"METRO40900M40900","0670098":"METRO42220M42220","0659451":"METRO40140M40140","0653322":"METRO41740M41740","0629000":"METRO31080MM5945","0827425":"METRO22660M22660","0640130":"METRO31080MM4480","0655156":"METRO31080MM4480","0664224":"METRO41500M41500","0616350":"METRO40140M40140","0633000":"METRO41860MM5775","0843000":"METRO19740M19740","0662938":"METRO40900M40900","0471510":"METRO38060M38060","0677000":"METRO41940M41940","0622804":"METRO41740M41740","0908000":"METRO14860M14860","0658072":"METRO31080MM4480","0877290":"METRO19740M19740","0682954":"METRO47300M47300","0680000":"METRO31080MM4480","0628000":"METRO31080MM5945","0653980":"METRO31080MM5945","0682590":"METRO40140M40140","0973000":"METRO14860M14860","0656000":"METRO31080MM4480","0952000":"METRO35300M35300","0669084":"METRO41940M41940","0672016":"METRO37100M37100","0678582":"METRO37100M37100","0614218":"METRO23420M23420","0681666":"METRO46700M46700","0616000":"METRO41860MM5775","0803455":"METRO19740M19740","0937000":"METRO25540M25540","0623182":"METRO46700M46700","0606000":"METRO41860MM5775","0602252":"METRO41860MM5775","0660620":"METRO41860MM5775","0883835":"METRO19740M19740","0980000":"METRO47930M47930","0611194":"METRO41740M41740","0650076":"METRO40140M40140","0620802":"METRO31080MM4480","0862000":"METRO39380M39380","0678120":"METRO40140M40140","0177256":"METRO46220M46220","0619766":"METRO31080MM4480","0832155":"METRO24540M24540","0669196":"METRO42200M42200","0616532":"METRO31080MM5945","0646842":"METRO40140M40140","0665042":"METRO37100M37100","0464210":"METRO38060M38060","0812815":"METRO19740M19740","0428380":"METRO38060M38060","0684200":"METRO31080MM4480","0637692":"METRO40140M40140","0807850":"METRO14500M14500","0622230":"METRO31080MM4480","0407940":"METRO38060M38060","0636546":"METRO31080MM4480","0608954":"METRO31080MM4480","0621712":"METRO41740M41740","0660466":"METRO40140M40140","0668252":"METRO41860MM7360","0681554":"METRO46700M46700","0836410":"METRO19740M19740","0613014":"METRO17020M17020","0617918":"METRO41860MM7360","0633434":"METRO40140M40140","0485540":"METRO49740M49740","0652526":"METRO31080MM4480","0845970":"METRO14500M14500","0523290":"METRO22220M22220","0682996":"METRO41740M41740","0680238":"METRO44700M44700","0602553":"METRO40900M40900","0668196":"METRO41740M41740","0659920":"METRO39820M39820","0611530":"METRO31080MM4480","0615044":"METRO31080MM4480","0613210":"METRO40140M40140","0135896":"METRO13820M13820","0404720":"METRO38060M38060","0955990":"METRO14860M14860","0648256":"METRO31080MM5945","0646898":"METRO32900M32900","0636448":"METRO40140M40140","0673080":"METRO31080MM4480","0633182":"METRO40140M40140","0670000":"METRO31080MM4480","0684550":"METRO31080MM5945","0524550":"METRO22900M22900","0645484":"METRO44700M44700","0566080":"METRO22220M22220","0668084":"METRO41860MM5775","0669070":"METRO42200M42200","0918430":"METRO14860M14860","0613588":"METRO40900M40900","0639496":"METRO31080MM5945","0685292":"METRO31080MM4480","0668378":"METRO41860MM5775","0641992":"METRO41860MM5775","0632548":"METRO31080MM4480","0624638":"METRO40900M40900","0651182":"METRO31080MM5945","0649670":"METRO41940M41940","0608786":"METRO31080MM5945","0660102":"METRO41860MM7360","0659444":"METRO40900M40900","0600884":"METRO31080MM4480","0103076":"METRO12220M12220","0656700":"METRO40140M40140","0535710":"METRO27860MM3700","0639892":"METRO31080MM4480","0681344":"METRO40140M40140","0812415":"METRO19740M19740","0680854":"METRO31080MM5945","0647766":"METRO41940M41940","0846465":"METRO22660M22660","0650258":"METRO34900M34900","0613214":"METRO40140M40140","0600562":"METRO41860MM5775","0657792":"METRO41860MM5775","0611390":"METRO40900M40900","0604982":"METRO31080MM4480","0423620":"METRO22380M22380","0809280":"METRO19740M19740","0657456":"METRO41860MM5775","0602364":"METRO40140M40140","0950370":"METRO25540M25540","0659962":"METRO40140M40140","0662364":"METRO40900M40900","0560410":"METRO22220M22220","0680812":"METRO33700M33700","0639486":"METRO40140M40140","0410670":"METRO46060M46060","0458150":"METRO38060M38060","0121184":"METRO20020M20020","0680644":"METRO47300M47300","0620018":"METRO41860MM5775","0621230":"METRO40140M40140","0610046":"METRO37100M37100","0603666":"METRO31080MM4480","0683346":"METRO41860MM5775","0686972":"METRO49700M49700","0660018":"METRO31080MM4480","0831660":"METRO24300M24300","0645022":"METRO23420M31460","0515190":"METRO30780M30780","0642202":"METRO44700M44700","0681204":"METRO41860MM5775","0655282":"METRO41940M41940","0686832":"METRO31080MM5945","0444410":"METRO38060M38060","0618100":"METRO40900MM9270","0816495":"METRO19740M19740","0608142":"METRO41860MM5775","0611964":"METRO41860MM5775","0644574":"METRO31080MM4480","0550450":"METRO30780M30780","0673262":"METRO41860MM7360","0639248":"METRO31080MM5945","0982660":"METRO25540M25540","0665084":"METRO31080MM5945","0658240":"METRO47300M47300","0639290":"METRO31080MM5945","0686328":"METRO40900MM9270","0857630":"METRO19740M19740","0669112":"METRO42100M42100","0622678":"METRO41740M41740","0908420":"METRO47930M47930","0648816":"METRO31080MM4480","0410530":"METRO38060M38060","0640004":"METRO41740M41740","0624477":"METRO31080MM4480","0946450":"METRO35300M35300","0668364":"METRO41860MM7360","0656924":"METRO31080MM4480","0145784":"METRO26620M26620","0631960":"METRO25260M25260","0628168":"METRO31080MM4480","0670224":"METRO41740M41740","0656784":"METRO42220M42220","0648914":"METRO31080MM4480","0629504":"METRO41940M41940","0650398":"METRO41740M41740","0617610":"METRO41940M41940","0439370":"METRO29420M29420","0505320":"METRO22220M22220","0120104":"METRO19460M19460","0444270":"METRO46060M46060","0633588":"METRO40140M40140","0604758":"METRO40140M40140","0625380":"METRO31080MM5945","0673430":"METRO31080MM4480","0982800":"METRO35300M35300","0667112":"METRO40140M40140","0602462":"METRO31080MM4480","0684816":"METRO40900MM9270","0687042":"METRO40140M40140","0631596":"METRO31080MM4480","0614890":"METRO40140M40140","0619192":"METRO31080MM4480","0636056":"METRO31080MM4480","0641474":"METRO40900M40900","0657526":"METRO31080MM5945","0411230":"METRO46060M46060","0652582":"METRO41860MM7360","0612048":"METRO40140M40140","0655618":"METRO31080MM4480","0655184":"METRO40140M40140","0683668":"METRO42100M42100","0618394":"METRO12540M12540","0600947":"METRO31080MM5945","0621880":"METRO40900M40900","0947515":"METRO35300M35300","0630014":"METRO31080MM4480","0922700":"METRO25540M25540","0624498":"METRO40900M40900","0662896":"METRO31080MM4480"},"names":{"METRO35620MM5600":"New York, NY HUD Metro FMR Area","METRO31080MM4480":"Los Angeles-Long Beach-Glendale, CA HUD Metro FMR Area","METRO16980M16980":"Chicago-Joliet-Naperville, IL HUD Metro FMR Area","METRO26420M26420":"Houston-The Woodlands-Sugar Land, TX HUD Metro FMR Area","METRO38060M38060":"Phoenix-Mesa-Chandler, AZ MSA","METRO37980M37980":"Philadelphia-Camden-Wilmington, PA-NJ-DE-MD MSA","METRO41700M41700":"San Antonio-New Braunfels, TX HUD Metro FMR Area","METRO41740M41740":"San Diego-Chula Vista-Carlsbad, CA MSA","METRO19100M19100":"Dallas, TX HUD Metro FMR Area","METRO41940M41940":"San Jose-Sunnyvale-Santa Clara, CA HUD Metro FMR Area","METRO12420M12420":"Austin-Round Rock-San Marcos, TX MSA","METRO27260M27260":"Jacksonville, FL HUD Metro FMR Area","METRO19100MM2800":"Fort Worth-Arlington, TX HUD Metro FMR Area","METRO18140M18140":"Columbus, OH HUD Metro FMR Area","METRO16740M16740":"Charlotte-Concord-Gastonia, NC-SC HUD Metro FMR Area","METRO26900M26900":"Indianapolis-Carmel, IN HUD Metro FMR Area","METRO41860MM7360":"San Francisco, CA HUD Metro FMR Area","METRO42660MM7600":"Seattle-Bellevue, WA HUD Metro FMR Area","METRO19740M19740":"Denver-Aurora-Centennial, CO MSA","METRO36420M36420":"Oklahoma City, OK HUD Metro FMR Area","METRO34980M34980":"Nashville-Davidson--Murfreesboro--Franklin, TN HUD Metro FMR Area","METRO47900M47900":"Washington-Arlington-Alexandria, DC-VA-MD HUD Metro FMR Area","METRO21340M21340":"El Paso, TX HUD Metro FMR Area","METRO14460MM1120":"Boston-Cambridge-Quincy, MA-NH HUD Metro FMR Area","METRO29820M29820":"Las Vegas-Henderson-North Las Vegas, NV MSA","METRO38900M38900":"Portland-Vancouver-Hillsboro, OR-WA MSA","METRO19820M19820":"Detroit-Warren-Livonia, MI HUD Metro FMR Area","METRO31140M31140":"Louisville, KY-IN HUD Metro FMR Area","METRO32820M32820":"Memphis, TN-MS-AR HUD Metro FMR Area","METRO12580M12580":"Baltimore-Columbia-Towson, MD MSA","METRO33340M33340":"Milwaukee-Waukesha, WI MSA","METRO10740M10740":"Albuquerque, NM MSA","METRO46060M46060":"Tucson, AZ MSA","METRO23420M23420":"Fresno, CA HUD Metro FMR Area","METRO40900M40900":"Sacramento--Roseville--Arden-Arcade, CA HUD Metro FMR Area","METRO28140M28140":"Kansas City, MO-KS HUD Metro FMR Area","METRO12060M12060":"Atlanta-Sandy Springs-Roswell, GA HUD Metro FMR Area","METRO36540M36540":"Omaha-Council Bluffs, NE-IA HUD Metro FMR Area","METRO17820M17820":"Colorado Springs, CO HUD Metro FMR Area","METRO39580M39580":"Raleigh-Cary, NC MSA","METRO33100MM5000":"Miami-Miami Beach-Kendall, FL HUD Metro FMR Area","METRO47260M47260":"Virginia Beach-Norfolk-Newport News, VA-NC HUD Metro FMR Area","METRO41860MM5775":"Oakland-Fremont, CA HUD Metro FMR Area","METRO33460M33460":"Minneapolis-St. Paul-Bloomington, MN-WI HUD Metro FMR Area","METRO46140M46140":"Tulsa, OK HUD Metro FMR Area","METRO12540M12540":"Bakersfield-Delano, CA MSA","METRO45300M45300":"Tampa-St. Petersburg-Clearwater, FL MSA","METRO48620M48620":"Wichita, KS HUD Metro FMR Area","METRO35380M35380":"New Orleans-Metairie, LA HUD Metro FMR Area","METRO17410N17460":"Cleveland, OH HUD Metro FMR Area","METRO46520M46520":"Urban Honolulu, HI MSA","METRO31080MM5945":"Santa Ana-Anaheim-Irvine, CA HUD Metro FMR Area","METRO30460M30460":"Lexington-Fayette, KY MSA","METRO44700M44700":"Stockton-Lodi, CA MSA","METRO36740M36740":"Orlando-Kissimmee-Sanford, FL MSA","METRO40140M40140":"Riverside-San Bernardino-Ontario, CA MSA","METRO41980MM7440":"San Juan-Guaynabo, PR HUD Metro FMR Area","METRO18580M18580":"Corpus Christi, TX HUD Metro FMR Area","METRO17140M17140":"Cincinnati, OH-KY-IN HUD Metro FMR Area","METRO35620MM5640":"Newark, NJ HUD Metro FMR Area","METRO38300M38300":"Pittsburgh, PA HUD Metro FMR Area","METRO24660M24660":"Greensboro-High Point, NC HUD Metro FMR Area","METRO30700M30700":"Lincoln, NE HUD Metro FMR Area","METRO35620MM3640":"Jersey City, NJ HUD Metro FMR Area","METRO20500M20500":"Durham-Chapel Hill, NC HUD Metro FMR Area","METRO11260M11260":"Anchorage, AK HUD Metro FMR Area","METRO41180M41180":"St. Louis, MO-IL HUD Metro FMR Area","METRO31540M31540":"Madison, WI HUD Metro FMR Area","METRO15380M15380":"Buffalo-Cheektowaga, NY MSA","METRO39900M39900":"Reno, NV HUD Metro FMR Area","METRO23060M23060":"Fort Wayne, IN HUD Metro FMR Area","METRO45780M45780":"Toledo, OH MSA","METRO31180M31180":"Lubbock, TX HUD Metro FMR Area","METRO29700M29700":"Laredo, TX MSA","METRO49180M49180":"Winston-Salem, NC HUD Metro FMR Area","METRO14260M14260":"Boise City, ID HUD Metro FMR Area","METRO38940M38940":"Port St. Lucie, FL MSA","METRO44060M44060":"Spokane, WA HUD Metro FMR Area","METRO40060M40060":"Richmond, VA HUD Metro FMR Area","METRO26620M26620":"Huntsville, AL MSA","METRO12940M12940":"Baton Rouge, LA HUD Metro FMR Area","METRO42660MM8200":"Tacoma, WA HUD Metro FMR Area","METRO33700M33700":"Modesto, CA MSA","METRO15980M15980":"Cape Coral-Fort Myers, FL MSA","METRO19780M19780":"Des Moines-West Des Moines, IA HUD Metro FMR Area","METRO22180M22180":"Fayetteville, NC HUD Metro FMR Area","METRO40380M40380":"Rochester, NY MSA","METRO41620M41620":"Salt Lake City, UT HUD Metro FMR Area","METRO49340M49340":"Worcester, MA HUD Metro FMR Area","METRO17980M17980":"Columbus, GA-AL HUD Metro FMR Area","METRO30780M30780":"Little Rock-North Little Rock-Conway, AR HUD Metro FMR Area","METRO33660M33660":"Mobile, AL MSA","METRO11100M11100":"Amarillo, TX HUD Metro FMR Area","METRO45220M45220":"Tallahassee, FL HUD Metro FMR Area","METRO12260M12260":"Augusta-Richmond County, GA-SC HUD Metro FMR Area","METRO43620M43620":"Sioux Falls, SD HUD Metro FMR Area","METRO37100M37100":"Oxnard-Thousand Oaks-Ventura, CA MSA","METRO24340M24340":"Grand Rapids-Wyoming, MI HUD Metro FMR Area","METRO13820M13820":"Birmingham-Hoover, AL HUD Metro FMR Area","METRO33860M33860":"Montgomery, AL MSA","METRO28940M28940":"Knoxville, TN HUD Metro FMR Area","METRO39300M39300":"Providence-Fall River, RI-MA HUD Metro FMR Area","METRO10420M10420":"Akron, OH MSA","METRO15180M15180":"Brownsville-Harlingen, TX MSA","METRO16860M16860":"Chattanooga, TN-GA MSA","METRO33100MM2680":"Fort Lauderdale, FL HUD Metro FMR Area","METRO43340M43340":"Shreveport-Bossier City, LA MSA","METRO21660M21660":"Eugene-Springfield, OR MSA","METRO41420M41420":"Salem, OR MSA","METRO42220M42220":"Santa Rosa-Petaluma, CA MSA","METRO17300M17300":"Clarksville, TN-KY HUD Metro FMR Area","METRO22660M22660":"Fort Collins-Loveland, CO MSA","METRO44180M44180":"Springfield, MO HUD Metro FMR Area","METRO41500M41500":"Salinas, CA MSA","METRO35620MM0875":"Bergen-Passaic, NJ HUD Metro FMR Area","METRO28660M28660":"Killeen-Temple, TX HUD Metro FMR Area","METRO31420M31420":"Macon-Bibb County, GA HUD Metro FMR Area","METRO44140M44140":"Springfield, MA MSA","METRO16700M16700":"Charleston-North Charleston, SC MSA","METRO14860M14860":"Bridgeport-Stamford-Danbury, CT MSA","METRO42340M42340":"Savannah, GA MSA","METRO40420M40420":"Rockford, IL MSA","METRO27140M27140":"Jackson, MS HUD Metro FMR Area","METRO45060M45060":"Syracuse, NY MSA","METRO23540M23540":"Gainesville, FL HUD Metro FMR Area","METRO32580M32580":"McAllen-Edinburg-Mission, TX MSA","METRO47300M47300":"Visalia, CA MSA","METRO47380M47380":"Waco, TX HUD Metro FMR Area","METRO17900M17900":"Columbia, SC HUD Metro FMR Area","METRO16300M16300":"Cedar Rapids, IA HUD Metro FMR Area","METRO33260M33260":"Midland, TX HUD Metro FMR Area","METRO19430M19430":"Dayton-Kettering-Beavercreek, OH MSA","METRO35300M35300":"New Haven, CT MSA","METRO22020M22020":"Fargo, ND-MN MSA","METRO37340M37340":"Palm Bay-Melbourne-Titusville, FL MSA","METRO17860M17860":"Columbia, MO HUD Metro FMR Area","METRO10180M10180":"Abilene, TX MSA","METRO26420MM1145":"Brazoria County, TX HUD Metro FMR Area","METRO12020M12020":"Athens-Clarke County, GA MSA","METRO10900M10900":"Allentown-Bethlehem-Easton, PA HUD Metro FMR Area","METRO45820M45820":"Topeka, KS MSA","METRO17780M17780":"College Station-Bryan, TX MSA","METRO46700M46700":"Vallejo, CA MSA","METRO40340M40340":"Rochester, MN HUD Metro FMR Area","METRO33100MM8960":"West Palm Beach-Boca Raton, FL HUD Metro FMR Area","METRO11460M11460":"Ann Arbor, MI MSA","METRO29180M29180":"Lafayette, LA HUD Metro FMR Area","METRO25540M25540":"Hartford-West Hartford-East Hartford, CT MSA","METRO48900M48900":"Wilmington, NC HUD Metro FMR Area","METRO29460M29460":"Lakeland-Winter Haven, FL MSA","METRO13740M13740":"Billings, MT HUD Metro FMR Area","METRO14460MM4560":"Lowell, MA HUD Metro FMR Area","METRO21780M21780":"Evansville, IN MSA","METRO31700MM4760":"Manchester, NH HUD Metro FMR Area","METRO36220M36220":"Odessa, TX MSA","METRO47930M47930":"Waterbury-Shelton, CT MSA","METRO39340M39340":"Provo-Orem-Lehi, UT MSA","METRO29740M29740":"Las Cruces, NM MSA","METRO44100M44100":"Springfield, IL MSA","METRO13140M13140":"Beaumont-Port Arthur, TX MSA","METRO29620M29620":"Lansing-East Lansing, MI MSA","METRO37900M37900":"Peoria, IL MSA","METRO39380M39380":"Pueblo, CO MSA","METRO46220M46220":"Tuscaloosa, AL HUD Metro FMR Area","METRO24540M24540":"Greeley, CO MSA","METRO42200M42200":"Santa Maria-Santa Barbara, CA MSA","METRO46340M46340":"Tyler, TX MSA","METRO38660M38660":"Ponce, PR HUD Metro FMR Area","METRO14500M14500":"Boulder, CO MSA","METRO24580M24580":"Green Bay, WI HUD Metro FMR Area","METRO14460MM1200":"Brockton, MA HUD Metro FMR Area","METRO13460M13460":"Bend-Redmond, OR HUD Metro FMR Area","METRO43780M43780":"South Bend-Mishawaka, IN HUD Metro FMR Area","METRO48660M48660":"Wichita Falls, TX MSA","METRO17020M17020":"Chico, CA MSA","METRO41100M41100":"St. George, UT MSA","METRO39300MM5400":"New Bedford, MA HUD Metro FMR Area","METRO19340M19340":"Davenport-Moline-Rock Island, IA-IL MSA","METRO10580M10580":"Albany-Schenectady-Troy, NY MSA","METRO49740M49740":"Yuma, AZ MSA","METRO41660M41660":"San Angelo, TX MSA","METRO28450M28450":"Kenosha, WI MSA","METRO22220M22220":"Fayetteville-Springdale-Rogers, AR MSA","METRO40220M40220":"Roanoke, VA HUD Metro FMR Area","METRO19660M37380":"Palm Coast, FL HUD Metro FMR Area","METRO19660M19660":"Deltona-Daytona Beach-Ormond Beach, FL HUD Metro FMR Area","METRO49420M49420":"Yakima, WA MSA","METRO29940M29940":"Lawrence, KS MSA","METRO39740M39740":"Reading, PA MSA","METRO35620MM5190":"Monmouth-Ocean, NJ HUD Metro FMR Area","METRO11700M11700":"Asheville, NC MSA","METRO21500M21500":"Erie, PA MSA","METRO13380M13380":"Bellingham, WA MSA","METRO39820M39820":"Redding, CA MSA","METRO24780M24780":"Greenville, NC MSA","METRO32900M32900":"Merced, CA MSA","METRO31700MM5350":"Nashua, NH HUD Metro FMR Area","METRO30020M30020":"Lawton, OK HUD Metro FMR Area","METRO45940M45940":"Trenton-Princeton, NJ MSA","METRO16580M16580":"Champaign-Urbana, IL HUD Metro FMR Area","METRO22900M22900":"Fort Smith, AR-OK MSA","METRO42140M42140":"Santa Fe, NM MSA","METRO14460MM4160":"Lawrence, MA-NH HUD Metro FMR Area","METRO36260M36260":"Ogden, UT MSA","METRO20260M20260":"Duluth, MN-WI MSA","METRO32780M32780":"Medford, OR MSA","METRO43580M43580":"Sioux City, IA-NE-SD MSA","METRO28420M28420":"Kennewick-Richland, WA MSA","METRO35840M35840":"North Port-Bradenton-Sarasota, FL MSA","METRO48680M48680":"Wildwood-The Villages, FL MSA","METRO47580M47580":"Warner Robins, GA HUD Metro FMR Area","METRO30980M30980":"Longview, TX HUD Metro FMR Area","METRO29340M29340":"Lake Charles, LA HUD Metro FMR Area","METRO12220M12220":"Auburn-Opelika, AL HUD Metro FMR Area","METRO22420M22420":"Flint, MI MSA","METRO27860MM3700":"Jonesboro, AR HUD Metro FMR Area","METRO14020MM1020":"Bloomington, IN HUD Metro FMR Area","METRO31340M31340":"Lynchburg, VA MSA","METRO14010M14010":"Bloomington, IL MSA","METRO34900M34900":"Napa, CA MSA","METRO39660M39660":"Rapid City, SD HUD Metro FMR Area","METRO39540M39540":"Racine-Mount Pleasant, WI MSA","METRO16980MM2960":"Gary, IN HUD Metro FMR Area","METRO33540M33540":"Missoula, MT HUD Metro FMR Area","METRO22380M22380":"Flagstaff, AZ MSA","METRO42540M42540":"Scranton--Wilkes-Barre, PA MSA","METRO26980MM3500":"Iowa City, IA HUD Metro FMR Area","METRO13900M13900":"Bismarck, ND MSA","METRO14540M14540":"Bowling Green, KY HUD Metro FMR Area","METRO11540M11540":"Appleton, WI MSA","METRO28020M28020":"Kalamazoo-Portage, MI MSA","METRO25060M25060":"Gulfport-Biloxi, MS HUD Metro FMR Area","METRO24860M24860":"Greenville-Mauldin-Easley, SC HUD Metro FMR Area","METRO41980MM1310":"Caguas, PR HUD Metro FMR Area","METRO27740M27740":"Johnson City, TN MSA","METRO20020M20020":"Dothan, AL HUD Metro FMR Area","METRO27340M27340":"Jacksonville, NC MSA","METRO41140M41140":"St. Joseph, MO-KS MSA","METRO29200M29140":"Lafayette-West Lafayette, IN HUD Metro FMR Area","METRO41060M41060":"St. Cloud, MN MSA","METRO20740M20740":"Eau Claire, WI MSA","METRO19500M19500":"Decatur, IL MSA","METRO15940M15940":"Canton-Massillon, OH MSA","METRO49700M49700":"Yuba City, CA MSA","METRO38860MM6400":"Portland, ME HUD Metro FMR Area","METRO27180M27180":"Jackson, TN HUD Metro FMR Area","METRO24300M24300":"Grand Junction, CO MSA","METRO23420M31460":"Madera County, CA HUD Metro FMR Area","METRO26820M26820":"Idaho Falls, ID HUD Metro FMR Area","METRO11180M11180":"Ames, IA HUD Metro FMR Area","METRO10500M10500":"Albany, GA MSA","METRO47940M47940":"Waterloo-Cedar Falls, IA HUD Metro FMR Area","METRO40900MM9270":"Yolo, CA HUD Metro FMR Area","METRO36780M36780":"Oshkosh-Neenah, WI MSA","METRO36100M36100":"Ocala, FL MSA","METRO39460M39460":"Punta Gorda, FL MSA","METRO27500M27500":"Janesville-Beloit, WI MSA","METRO47020M47020":"Victoria, TX MSA","METRO16940M16940":"Cheyenne, WY MSA","METRO34620M34620":"Muncie, IN MSA","METRO46540M46540":"Utica-Rome, NY MSA","METRO42100M42100":"Santa Cruz-Watsonville, CA MSA","METRO47220M47220":"Vineland, NJ MSA","METRO39300MM1120":"Taunton-Mansfield-Norton, MA HUD Metro FMR Area","METRO24500M24500":"Great Falls, MT MSA","METRO36980M36980":"Owensboro, KY MSA","METRO18700M18700":"Corvallis, OR MSA","METRO25260M25260":"Hanford-Corcoran, CA MSA","METRO15500M15500":"Burlington, NC MSA","METRO49660M49660":"Youngstown-Warren, OH MSA","METRO20220M20220":"Dubuque, IA MSA","METRO29020M29020":"Kokomo, IN MSA","METRO24220M24220":"Grand Forks, ND-MN MSA","METRO32420M32420":"Mayagüez, PR HUD Metro FMR Area","METRO16220M16220":"Casper, WY MSA","METRO35620MM5380":"Nassau-Suffolk, NY HUD Metro FMR Area","METRO45460M45460":"Terre Haute, IN HUD Metro FMR Area","METRO16180M16180":"Carson City, NV MSA","METRO29420M29420":"Lake Havasu City-Kingman, AZ MSA","METRO44220M44220":"Springfield, OH MSA","METRO36500M36500":"Olympia-Lacey-Tumwater, WA MSA","METRO29540M29540":"Lancaster, PA MSA","METRO38540M38540":"Pocatello, ID MSA","METRO19460M19460":"Decatur, AL MSA","METRO10540M10540":"Albany, OR MSA","METRO17660M17660":"Coeur d'Alene, ID MSA","METRO35620MM5015":"Middlesex-Somerset-Hunterdon, NJ HUD Metro FMR Area","METRO28700M28700":"Kingsport-Bristol, TN-VA MSA","METRO14580M14580":"Bozeman, MT MSA","METRO34980N47119":"Maury County, TN HUD Metro FMR Area","METRO26900M11300":"Anderson, IN HUD Metro FMR Area","METRO46660M46660":"Valdosta, GA MSA","METRO30860M30860":"Logan, UT-ID MSA","METRO40580M40580":"Rocky Mount, NC MSA","METRO31740M31740":"Manhattan, KS HUD Metro FMR Area","METRO46300N16083":"Twin Falls County, ID HUD Metro FMR Area","METRO37860M37860":"Pensacola-Ferry Pass-Brent, FL MSA","METRO21140M21140":"Elkhart-Goshen, IN MSA","METRO24260N31079":"Hall County, NE HUD Metro FMR Area","METRO27900M27900":"Joplin, MO HUD Metro FMR Area","METRO16740N37097":"Iredell County, NC HUD Metro FMR Area","METRO12980M12980":"Battle Creek, MI MSA","METRO18020M18020":"Columbus, IN MSA","METRO25500M25500":"Harrisonburg, VA MSA","METRO29100M29100":"La Crosse-Onalaska, WI-MN HUD Metro FMR Area","METRO21420M21420":"Enid, OK MSA","NCNTY15001N15001":"Hawaii County, HI","METRO25420M25420":"Harrisburg-Carlisle, PA MSA","METRO16620M16620":"Charleston, WV HUD Metro FMR Area","METRO15540M15540":"Burlington-South Burlington, VT MSA"}}
ddiff --git a/server/housing/fair-market-rents.js b/server/housing/fair-market-rents.js
nnew file mode 100644
iindex 0000000..cb23f8a
---- /dev/null
++++ b/server/housing/fair-market-rents.js
@@@ -0,0 +1,132 @@
++'use strict';
++
++/**
++ * HUD's Fair Market Rents, read from OpenVibe.Sources (source hud-fair-market-rents, category housing), never from HUD:
++ * Sources reads HUD's own FMR layer every week under its robots and terms checks, and Rent follows Sources' change
++ * feed (openvibe-publishing/ingest: the cursor in rent_ingest_cursor, one transaction per page) into rent_fmr_areas,
++ * one row per FMR area as HUD states it. An item Sources removes is deleted here.
++ *
++ * A Fair Market Rent is HUD's estimate of what a modest rental costs in an area, rent plus essential utilities (the
++ * 40th percentile, for most areas), from a studio to four bedrooms. HUD sets them each October; Housing Choice
++ * Vouchers pay up to a payment standard between 90% and 110% of them.
++ *
++ * The city pages ask forCity(city) synchronously: every area (about 2,600) is held in memory, loaded at start and
++ * after a pull that changed something. A city finds its area through server/data/city-fmr-areas.json (built by
++ * tools/build-fmr-areas.js from each city's Census point).
++ *
++ * Pulls 30 seconds after start and then every six hours. Off without OV_SOURCES_INTERNAL_URL and the OAuth client
++ * secret (a test passes its own source).
++ */
++const ingest = require('openvibe-publishing/ingest');
++const AREAS = require('../data/city-fmr-areas.json');
++
++const SOURCE_KEY = 'hud-fair-market-rents';
++const CURSOR = 'sources.housing.after';
++const EVERY_MS = 6 * 60 * 60 * 1000;
++const FIRST_MS = 30 * 1000;
++const CODE_RE = /^[A-Z0-9]{6,40}$/;
++const SIZES = [['studio', 'fmr_0br'], ['b1', 'fmr_1br'], ['b2', 'fmr_2br'], ['b3', 'fmr_3br'], ['b4', 'fmr_4br']];
++const HUD_URL = 'https://www.huduser.gov/portal/datasets/fmr.html';
++
++const dollars = (v) => (Number.isInteger(v) ? v : (typeof v === 'string' && /^\d{1,6}$/.test(v) ? Number(v) : null));
++const okRent = (v) => { const n = dollars(v); return n != null && n > 0 && n < 100000 ? n : null; };
++
++/** A Sources item → a rent_fmr_areas row, or null when it is not an FMR area this service can show. */
++function rowOf(item) {
++ const f = (item && item.fields) || {};
++ const code = item && typeof item.identity === 'string' ? item.identity.trim() : '';
++ const name = item && typeof item.title === 'string' ? item.title.replace(/\s+/g, ' ').trim().slice(0, 200) : '';
++ if (!CODE_RE.test(code) || !name) return null;
++ const row = {
++ code, item_id: String(item.id), revision: Number(item.revision) || 1, name,
++ fmr_0br: okRent(f.fmr_0br), fmr_1br: okRent(f.fmr_1br), fmr_2br: okRent(f.fmr_2br), fmr_3br: okRent(f.fmr_3br), fmr_4br: okRent(f.fmr_4br),
++ retrieved_at: (item.provenance && item.provenance.retrieved_at) || new Date(0).toISOString(),
++ };
++ return SIZES.some(([, col]) => row[col] != null) ? row : null;
++}
++
++const view = (r) => ({ code: r.code, name: r.name, rents: Object.fromEntries(SIZES.map(([k, col]) => [k, r[col]])), retrieved_at: r.retrieved_at });
++
++function createFairMarketRents({ config, s, fetchImpl = globalThis.fetch, log = console, source = null }) {
++ const url = String((config.sources && config.sources.internalUrl) || '').replace(/\/+$/, '');
++ const client = source || (url && config.oauth.clientSecret
++ ? ingest.createSourcesClient({ config: { ...config, sources: { internalUrl: url, category: 'housing' } }, fetchImpl })
++ : null);
++ const cursor = ingest.createChangeCursor(s.db, { prefix: 'rent', now: s.now });
++ const stats = { pulls: 0, applied: 0, removed: 0, last_at: null, last_error: null };
++ let byCode = new Map();
++ let timer = null;
++ let first = null;
++ let running = false;
++
++ async function apply(item, t) {
++ if (!item || item.source_key !== SOURCE_KEY || item.kind !== 'rent_area') return 'hold';
++ if (item.removed) {
++ await t.query('DELETE FROM rent_fmr_areas WHERE item_id = $1', [String(item.id)]);
++ return 'removed';
++ }
++ const row = rowOf(item);
++ if (!row) return 'hold';
++ const cols = Object.keys(row);
++ // A replayed page (or an older revision arriving late) never takes a row back.
++ await t.query(`INSERT INTO rent_fmr_areas (${cols.join(', ')}) VALUES (${cols.map((_, i) => `$${i + 1}`).join(', ')})
++ ON CONFLICT (code) DO UPDATE SET ${cols.filter((c) => c !== 'code').map((c) => `${c} = EXCLUDED.${c}`).join(', ')}
++ WHERE rent_fmr_areas.item_id <> EXCLUDED.item_id OR rent_fmr_areas.revision <= EXCLUDED.revision`, cols.map((c) => row[c]));
++ return 'applied';
++ }
++
++ /** Every area into memory (the city pages read it synchronously). */
++ async function load() {
++ const rows = await s.db.many('SELECT * FROM rent_fmr_areas');
++ byCode = new Map(rows.map((r) => [r.code, view(r)]));
++ return byCode.size;
++ }
++
++ /** Follow Sources' change feed to its end, then reload what changed. Never throws: the caller is a timer or a test. */
++ async function pull() {
++ if (!client || running) return { skipped: true };
++ running = true;
++ try {
++ const out = await ingest.pullChanges({ db: s.db, cursor, source: client, name: CURSOR, apply });
++ stats.pulls++;
++ stats.applied += out.applied;
++ stats.removed += out.removed;
++ stats.last_at = s.iso();
++ stats.last_error = null;
++ if (out.applied || out.removed || !byCode.size) await load();
++ if (out.applied || out.removed) log.log(`[FMR] ${out.applied} areas updated, ${out.removed} removed (Sources cursor ${out.after})`);
++ return out;
++ } catch (err) {
++ stats.last_error = String((err && err.message) || err).slice(0, 300);
++ log.warn(`[FMR] pull from OpenVibe.Sources failed: ${stats.last_error}`);
++ return { error: stats.last_error };
++ } finally { running = false; }
++ }
++
++ return {
++ enabled: Boolean(client),
++ stats,
++ pull,
++ apply,
++ load,
++ /** The FMR area a city lies in, with HUD's rents by size, or null. */
++ forCity(city) {
++ const code = city && AREAS.areas[city.id];
++ return (code && byCode.get(code)) || null;
++ },
++ size: () => byCode.size,
++ start() {
++ if (timer) return false;
++ load().catch((err) => log.warn(`[FMR] load failed: ${(err && err.message) || err}`));
++ if (!client) return false;
++ timer = setInterval(() => { pull().catch(() => {}); }, EVERY_MS);
++ timer.unref();
++ first = setTimeout(() => { pull().catch(() => {}); }, FIRST_MS);
++ first.unref();
++ return true;
++ },
++ stop() { clearInterval(timer); clearTimeout(first); timer = null; first = null; },
++ };
++}
++
++module.exports = { createFairMarketRents, rowOf, SOURCE_KEY, SIZES, HUD_URL };
ddiff --git a/server/http/city-pages.js b/server/http/city-pages.js
iindex ada148b..a45f16a 100644
---- a/server/http/city-pages.js
++++ b/server/http/city-pages.js
@@@ -10,6 +10,9 @@
* GET /in/:state a state's cities and their typical rents by size
* GET /in/:state/:city one city: typical rents, a fair-rent check, every site's search, what is here, nearby
* GET /fair is a rent fair? (city, bedrooms, monthly rent) → where it sits against the median
++ *
++ * Where HUD's Fair Market Rents are known for a city's area (server/housing/fair-market-rents.js, from OpenVibe.Sources),
++ * the city page shows them beside the Census medians and the check names the one for that size.
*/
const seo = require('openvibe-shared/seo');
@@@ -30,8 +33,12 @@ const cell = (c, key) => {
return v ? html`${v}${c.moe[key] && c.rent[key] < 3501 ? html` <span class="moe">±$${c.moe[key].toLocaleString('en-US')}</span>` : ''}` : html`<span class="muted">not published</span>`;
};
const placeLabel = (c) => `${c.name}, ${c.state}`;
++const { HUD_URL } = require('../housing/fair-market-rents');
++const fmrText = (n) => (n == null ? null : `$${n.toLocaleString('en-US')}`);
++/** What a Fair Market Rent is, under the city table (HUD's own definition and use, in short). */
++const fmrSource = (area) => html`<p class="small muted city-source">HUD Fair Market Rent for the ${area.name}: what a modest rental there costs, rent plus essential utilities, by HUD's estimate. HUD sets them each October; Housing Choice Vouchers pay up to 90% to 110% of them. <a href="${HUD_URL}" rel="noopener">HUD</a> (public domain), through OpenVibe.Sources.</p>`;
--function mountCityPages(r, { config, page }) {
++function mountCityPages(r, { config, page, fmr = null }) {
const base = () => String(config.baseUrl).replace(/\/+$/, '');
const crumbsLd = (items) => seo.jsonLd.breadcrumbs(items.map((i) => ({ name: i.label, url: `${base()}${i.href}` })));
@@@ -96,6 +103,7 @@ ${source()}
const hostOf = (u) => { try { return new URL(u).hostname.replace(/^www\./, ''); } catch { return ''; } };
const near = cities.nearby(c, 8);
const two = cities.rentText(c.rent.b2);
++ const area = fmr ? fmr.forCity(c) : null;
page(req, res, {
title: `Rent in ${where}: typical rents and every rental site`,
description: `${two ? `A typical 2-bedroom in ${where} rents for ${two} a month. ` : ''}Every rental site's search for ${c.name} in one place: Craigslist, Zillow, Apartments.com, Zumper and more, with typical rents by size and affordable housing nearby.`,
@@@ -107,10 +115,14 @@ ${source()}
<p class="sc-lede">Every rental site's search for ${c.name} in one place, what rent usually costs here, and a quick check for a price you were quoted.</p>
<div class="city-top">
<section class="rent-card" aria-labelledby="h-typical"><h2 id="h-typical">What rent costs in ${c.name}</h2>
--<table class="rent-sizes"><caption class="sr-only">Median gross rent by size in ${where}</caption><tbody>
++${area ? html`<table class="rent-sizes rent-sizes-fmr"><caption class="sr-only">Median gross rent (Census) and HUD Fair Market Rent by size in ${where}</caption>
++<thead><tr><td></td><th scope="col">Middle rent</th><th scope="col">HUD fair market</th></tr></thead><tbody>
++${cities.SIZES.map((s) => html`<tr><th scope="row">${s.text}</th><td>${cell(c, s.key)}</td><td>${fmrText(area.rents[s.key]) || html`<span class="muted">—</span>`}</td></tr>`)}
++<tr class="rent-all"><th scope="row">All rentals</th><td>${cell(c, 'all')}</td><td></td></tr></tbody></table>
++${source()}${fmrSource(area)}` : html`<table class="rent-sizes"><caption class="sr-only">Median gross rent by size in ${where}</caption><tbody>
${cities.SIZES.map((s) => html`<tr><th scope="row">${s.text}</th><td>${cell(c, s.key)}</td></tr>`)}
<tr class="rent-all"><th scope="row">All rentals</th><td>${cell(c, 'all')}</td></tr></tbody></table>
--${source()}</section>
++${source()}`}</section>
<section class="fair-card" aria-labelledby="h-fair"><h2 id="h-fair">Is a rent fair?</h2><p class="small muted">Compare a price from any site with the middle rent in ${c.name}.</p>${fairForm({}, { city: c })}</section>
</div>
<section class="sc-sec" aria-labelledby="h-sites"><h2 id="h-sites">Apartments in ${c.name}, on every site</h2>
@@@ -164,6 +176,7 @@ ${source()}`,
/** The verdict and a bar: the middle rent with its margin, and where the asked rent falls. */
function fairResult(city, f, rent) {
++ const area = fmr ? fmr.forCity(city) : null;
const max = Math.max(rent, f.median) * 1.35;
const pct = (n) => Math.max(0, Math.min(100, (n / max) * 100)).toFixed(1);
const lo = Math.max(0, f.median - f.moe); const hi = f.median + f.moe;
@@@ -172,6 +185,7 @@ ${source()}`,
<p>${f.text}</p>
<div class="fair-bar" aria-hidden="true"><span class="fair-band" style="left:${pct(lo)}%;width:${(pct(hi) - pct(lo)).toFixed(1)}%"></span><span class="fair-median" style="left:${pct(f.median)}%"></span><span class="fair-you" style="left:${pct(rent)}%"></span></div>
<p class="fair-legend small"><span class="lg lg-median"></span> Middle rent ${f.topped ? '$3,500 or more' : `$${f.median.toLocaleString('en-US')}`}${f.moe && !f.topped ? ` (±$${f.moe.toLocaleString('en-US')})` : ''} <span class="lg lg-you"></span> This rent $${rent.toLocaleString('en-US')}</p>
++${area && fmrText(area.rents[f.size.key]) ? html`<p class="small">HUD's Fair Market Rent for ${f.size.long} in the ${area.name} is ${fmrText(area.rents[f.size.key])} a month, rent plus essential utilities: the figure a Housing Choice Voucher is measured against.</p>` : ''}
<p class="small"><a href="${city.path}">Typical rents and every rental site in ${city.name}</a></p>
</section>`;
}
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex f8078e4..3ae84a5 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -886,7 +886,7 @@ ${it.newCount ? html` <span class="badge info">${it.newCount} new</span>` : html
// ── The board: listings kept from any site (http/board-pages.js) ──
mountBoardPages(r, { config, s, page, signedIn, requesterOf, mustSignIn, budgeted, sameSite, signInPrompt });
-- mountCityPages(r, { config, page });
++ mountCityPages(r, { config, page, fmr: ctx.fmr });
// ── Reporting ────────────────────────────────────────────
r.post('/listings/:id/reports', mustSignIn('/listings'), budgeted('rent.listing.report'), async (req, res) => {
ddiff --git a/server/index.js b/server/index.js
iindex 2e221b7..e3aa69a 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -42,6 +42,7 @@ async function start() {
ctx.keys.client.start();
ctx.search.start();
ctx.screen.start(); // the AI listing screen's sweep (off without OpenVibe.AI)
++ ctx.fmr.start(); // HUD's Fair Market Rents: loaded now, then followed from OpenVibe.Sources every six hours
// One sweep at boot (a restart must not leave a stale listing up a moment longer than it should), then a timer.
const expiry = startExpiryTimer(ctx.s);
@@@ -54,7 +55,7 @@ async function start() {
// Subscribe to the two ADR-033 topics at OpenVibe.Events (idempotent; off without RENT_EVENTS_URL and
// RENT_EVENTS_SECRET). The consumer itself is mounted in server/app.js.
const subscriptions = require('./events-consumer').startSubscriptions({ config, port: config.port, secret: config.events.secrets[0] || '' });
-- const extra = [() => { if (subscriptions) subscriptions.stop(); }];
++ const extra = [() => { if (subscriptions) subscriptions.stop(); }, () => ctx.fmr.stop()];
createLifecycle({ server, ctx, timers: [expiry.timer, ctx.housingPruneTimer, ...(hpiJob ? [hpiJob.timer] : [])], extra });
return { server, ctx, expiry };
}
ddiff --git a/test/fair-market-rents.test.js b/test/fair-market-rents.test.js
nnew file mode 100644
iindex 0000000..46862d2
---- /dev/null
++++ b/test/fair-market-rents.test.js
@@@ -0,0 +1,106 @@
++'use strict';
++/**
++ * HUD's Fair Market Rents (server/housing/fair-market-rents.js, server/data/city-fmr-areas.json, city-pages.js):
++ * - a Sources item of hud-fair-market-rents becomes one row per FMR area, as HUD states it; anything else is held;
++ * - the pull follows the change feed from the cursor; a removed area is deleted; an older revision never wins;
++ * - a city page shows HUD's rents beside the Census medians with what they are; /fair names the one for that size;
++ * a city whose area is unknown keeps the plain table.
++ */
++const assert = require('assert');
++const { boot, check, done } = require('./helpers/boot');
++const cities = require('../server/aggregate/cities');
++const AREAS = require('../server/data/city-fmr-areas.json');
++const { rowOf } = require('../server/housing/fair-market-rents');
++
++const ny = cities.find('ny', 'new-york');
++const austin = cities.find('tx', 'austin');
++const NY = AREAS.areas[ny.id];
++const AUSTIN = AREAS.areas[austin.id];
++
++const item = (seq, code, name, rents, over = {}) => ({
++ id: `itm_${seq}`, source_key: 'hud-fair-market-rents', category: 'housing', kind: 'rent_area', identity: code, canonical_url: null,
++ title: name, summary: null, fields: { fmr_0br: rents[0], fmr_1br: rents[1], fmr_2br: rents[2], fmr_3br: rents[3], fmr_4br: rents[4] },
++ revision: 1, provenance: { retrieved_at: '2026-10-22T10:00:00.000Z' }, removed: null, change_seq: seq, ...over,
++});
++
++function feed(items) {
++ return {
++ items,
++ async listItems({ after = 0, limit = 100 }) {
++ const rest = items.filter((x) => x.change_seq > after).sort((a, b) => a.change_seq - b.change_seq);
++ const page = rest.slice(0, limit);
++ return { items: page, next_after: page.length ? page[page.length - 1].change_seq : after, more: rest.length > limit, sources: {} };
++ },
++ };
++}
++
++(async () => {
++ const source = feed([
++ item(1, NY, 'New York, NY HUD Metro FMR Area', [2529, 2655, 2910, 3644, 3959]),
++ item(2, AUSTIN, 'Austin-Round Rock-San Marcos, TX MSA', [1474, 1562, 1852, 2347, 2760]),
++ { ...item(3, 'METRO10180M10180', 'Abilene, TX MSA', [1010, 1020, 1276, 1653, 1987]), kind: 'record' },
++ { ...item(4, 'X', 'Bad code', [1, 2, 3, 4, 5]) },
++ ]);
++ const t = await boot({ fmrSource: source });
++ try {
++ await check('the data: every city is placed in an FMR area by its Census point; New York and Austin in their metros', () => {
++ assert.ok(Object.keys(AREAS.areas).length >= cities.cities.length - 5, `${Object.keys(AREAS.areas).length} of ${cities.cities.length}`);
++ assert.strictEqual(NY, 'METRO35620MM5600');
++ assert.ok(/^METRO12420/.test(AUSTIN), AUSTIN);
++ assert.ok(Object.values(AREAS.areas).every((code) => /^[A-Z0-9]{6,40}$/.test(code)));
++ });
++
++ await check('an area becomes one row as HUD states it; a bad code or no rents is not an area', () => {
++ const row = rowOf(item(9, NY, 'New York, NY HUD Metro FMR Area', [2529, 2655, 2910, 3644, 3959]));
++ assert.deepStrictEqual([row.code, row.fmr_2br, row.fmr_4br], [NY, 2910, 3959]);
++ assert.strictEqual(rowOf(item(9, 'not a code!', 'x', [1, 2, 3, 4, 5])), null);
++ assert.strictEqual(rowOf(item(9, NY, 'x', [null, '', -4, 'abc', 0])), null);
++ assert.strictEqual(rowOf(item(9, NY, 'x', ['1500', null, null, null, null])).fmr_0br, 1500);
++ });
++
++ await check('before the first pull a city page keeps the Census table alone', async () => {
++ const page = await t.get('/in/ny/new-york');
++ assert.strictEqual(page.status, 200);
++ assert.ok(!page.text.includes('HUD fair market'));
++ });
++
++ await check('the pull follows the change feed and keeps the cursor; the areas are in memory for the pages', async () => {
++ const out = await t.ctx.fmr.pull();
++ assert.deepStrictEqual([out.applied, out.hold], [2, 2]);
++ assert.strictEqual(Number(await t.ctx.s.db.value("SELECT cursor FROM rent_ingest_cursor WHERE name = 'sources.housing.after'")), 4);
++ assert.deepStrictEqual(t.ctx.fmr.forCity(ny).rents, { studio: 2529, b1: 2655, b2: 2910, b3: 3644, b4: 3959 });
++ assert.strictEqual(t.ctx.fmr.forCity(cities.find('ca', 'los-angeles')), null);
++ });
++
++ await check('a city page shows HUD\'s rents beside the medians and says what they are', async () => {
++ const page = await t.get('/in/ny/new-york');
++ assert.ok(page.text.includes('HUD fair market'));
++ assert.ok(page.text.includes('$2,910'));
++ assert.ok(page.text.includes('New York, NY HUD Metro FMR Area'));
++ assert.ok(page.text.includes('Housing Choice Vouchers'));
++ const la = await t.get('/in/ca/los-angeles');
++ assert.ok(!la.text.includes('HUD fair market'), 'an area Sources has not given keeps the plain table');
++ });
++
++ await check('/fair names HUD\'s Fair Market Rent for that size', async () => {
++ const page = await t.get('/fair?city=tx/austin&beds=2&rent=1900');
++ assert.ok(page.text.includes('HUD&#39;s Fair Market Rent for a 2-bedroom') || page.text.includes('HUD\'s Fair Market Rent for a 2-bedroom'));
++ assert.ok(page.text.includes('$1,852'));
++ const five = await t.get('/fair?city=tx/austin&beds=5&rent=4000');
++ assert.ok(!five.text.includes('Fair Market Rent for'), 'HUD gives no 5-bedroom figure, and none is made up');
++ });
++
++ await check('a removed area is deleted; an older revision arriving late changes nothing', async () => {
++ source.items.push(item(10, AUSTIN, 'Austin-Round Rock-San Marcos, TX MSA', [1500, 1600, 1900, 2400, 2800], { id: 'itm_2', revision: 3 }));
++ source.items.push(item(11, AUSTIN, 'Austin-Round Rock-San Marcos, TX MSA', [1, 1, 1, 1, 1], { id: 'itm_2', revision: 2 }));
++ source.items.push({ ...item(12, NY, 'New York, NY HUD Metro FMR Area', [1, 1, 1, 1, 1]), id: 'itm_1', revision: 2, removed: { at: '2026-10-22T11:00:00.000Z', reason: 'licence' } });
++ const out = await t.ctx.fmr.pull();
++ assert.strictEqual(out.removed, 1);
++ assert.strictEqual(t.ctx.fmr.forCity(ny), null);
++ assert.strictEqual(t.ctx.fmr.forCity(austin).rents.b2, 1900);
++ });
++ } finally {
++ await t.close();
++ }
++ done();
++})();
ddiff --git a/test/helpers/boot.js b/test/helpers/boot.js
iindex 2564f01..58ffd63 100644
---- a/test/helpers/boot.js
++++ b/test/helpers/boot.js
@@@ -38,7 +38,7 @@ async function boot(opts = {}) {
const { createStore } = require('../../server/db');
const testdb = await require('./db').testDb();
const store = createStore(testdb.db, { now: opts.now });
-- const built = await createApp({ config, store, log, ai: opts.ai, telemetrySink: opts.telemetrySink, indexnow: opts.indexnow, limitsNow: opts.limitsNow, callerLimits: opts.callerLimits === true, accountSend: opts.accountSend, housingFetchImpl: opts.housingFetchImpl });
++ const built = await createApp({ config, store, log, ai: opts.ai, telemetrySink: opts.telemetrySink, indexnow: opts.indexnow, limitsNow: opts.limitsNow, callerLimits: opts.callerLimits === true, accountSend: opts.accountSend, housingFetchImpl: opts.housingFetchImpl, fmrSource: opts.fmrSource });
await built.ctx.keys.ensure();
const server = await new Promise((resolve) => { const s = http.createServer(built.app); s.listen(0, '127.0.0.1', () => resolve(s)); });
const base = `http://127.0.0.1:${server.address().port}`;
ddiff --git a/tools/build-fmr-areas.js b/tools/build-fmr-areas.js
nnew file mode 100644
iindex 0000000..b824589
---- /dev/null
++++ b/tools/build-fmr-areas.js
@@@ -0,0 +1,81 @@
++#!/usr/bin/env node
++'use strict';
++/**
++ * Builds server/data/city-fmr-areas.json: the HUD Fair Market Rent area each city in server/data/cities.json lies in
++ * (its FMR_CODE, e.g. METRO35620MM5600 for New York), so a city page can show HUD's Fair Market Rents for it. The
++ * rents themselves are not stored here: OpenVibe.Sources reads them from HUD every week (source hud-fair-market-rents,
++ * identity FMR_CODE) and Rent follows it (server/housing/fair-market-rents.js). Areas change only when HUD redraws
++ * them (with OMB's metro definitions, every few years), so this is run again then, not on a schedule.
++ *
++ * node tools/build-fmr-areas.js # about 30 minutes: one point query at a time, a second apart at least
++ * node tools/build-fmr-areas.js --only-missing # ask again only for the cities the last run left out
++ *
++ * Each city's internal point (the Census gazetteer's, already in cities.json) is asked of HUD's own Fair Market Rents
++ * feature layer (the one Sources reads, public domain), with OpenVibe.Rent's User-Agent. A point no area contains (a
++ * coastal city whose point lies on the water, like Miami Beach's) is asked again with a 3 km circle, and kept only when
++ * exactly one area answers; otherwise it is left out, never guessed.
++ */
++const fs = require('fs');
++const path = require('path');
++
++const LAYER = 'https://services.arcgis.com/VTyQ9soqVukalItT/arcgis/rest/services/Fair_Market_Rents/FeatureServer/0/query';
++const OUT = path.join(__dirname, '..', 'server', 'data', 'city-fmr-areas.json');
++const UA = 'OpenVibeRent/0.1 (+https://openvibe.rent)';
++const PACE_MS = 1000;
++
++async function areaAt(lat, lon, { radiusM = 0 } = {}) {
++ const url = new URL(LAYER);
++ url.search = new URLSearchParams({
++ geometry: `${lon},${lat}`, geometryType: 'esriGeometryPoint', inSR: '4326', spatialRel: 'esriSpatialRelIntersects',
++ ...(radiusM ? { distance: String(radiusM), units: 'esriSRUnit_Meter' } : {}),
++ outFields: 'FMR_CODE,FMR_AREANAME', returnGeometry: 'false', f: 'json',
++ }).toString();
++ for (let attempt = 1; ; attempt++) {
++ try {
++ const res = await fetch(url, { headers: { 'user-agent': UA, accept: 'application/json' }, signal: AbortSignal.timeout(30_000) });
++ if (!res.ok) throw new Error(`HTTP ${res.status}`);
++ const body = await res.json();
++ if (body.error) throw new Error(JSON.stringify(body.error).slice(0, 200));
++ const codes = [...new Set((body.features || []).map((f) => f.attributes.FMR_CODE))];
++ if (codes.length !== 1) return null;
++ const f = body.features[0];
++ return { code: f.attributes.FMR_CODE, name: f.attributes.FMR_AREANAME };
++ } catch (err) {
++ if (attempt >= 3) throw err;
++ await new Promise((r) => setTimeout(r, 5000 * attempt));
++ }
++ }
++}
++
++async function main() {
++ const { cities } = require('../server/data/cities.json');
++ const onlyMissing = process.argv.includes('--only-missing');
++ const before = onlyMissing ? JSON.parse(fs.readFileSync(OUT, 'utf8')) : { areas: {}, names: {} };
++ const areas = { ...before.areas };
++ const names = { ...before.names };
++ let missing = 0;
++ for (const [i, c] of cities.entries()) {
++ if (areas[c.id]) continue;
++ const started = Date.now();
++ const a = (await areaAt(c.lat, c.lon)) || (await areaAt(c.lat, c.lon, { radiusM: 3000 }));
++ if (a && /^[A-Z0-9]{6,40}$/.test(a.code)) { areas[c.id] = a.code; names[a.code] = a.name; } else { missing++; console.warn(`no area: ${c.name}, ${c.state}`); }
++ if ((i + 1) % 50 === 0) console.log(`${i + 1}/${cities.length}`);
++ const wait = PACE_MS - (Date.now() - started);
++ if (wait > 0) await new Promise((r) => setTimeout(r, wait));
++ }
++ const out = {
++ source: {
++ name: 'U.S. Department of Housing and Urban Development, Fair Market Rents feature layer',
++ url: 'https://hudgis-hud.opendata.arcgis.com/datasets/fair-market-rents',
++ licence: 'public domain (US federal government work)',
++ built: new Date().toISOString().slice(0, 10),
++ note: 'Each city\'s Census internal point, located in HUD\'s FMR areas. The rents come from OpenVibe.Sources (hud-fair-market-rents), not from this file.',
++ },
++ areas,
++ names,
++ };
++ fs.writeFileSync(OUT, `${JSON.stringify(out)}\n`);
++ console.log(`${Object.keys(areas).length} cities in ${Object.keys(names).length} areas; ${missing} without an area → ${OUT}`);
++}
++
++main().catch((err) => { console.error(err); process.exit(1); });