Generates location-specific industry benchmark defaults for every property at creation time, stored in the `properties.research_values` JSONB colum...
Generates location-specific industry benchmark defaults for every property at creation time, stored in the properties.research_values JSONB column. This replaces generic national averages with market-appropriate ranges before any AI research runs.
Seed profiles provide location-based financial benchmarks (tax rates, cost structures, ADR ranges) that apply to the hospitality asset class defined by globalAssumptions.propertyLabel. The profiles themselves are not asset-type-specific — they represent regional market conditions. When the asset type changes (e.g., from "Boutique Hotel" to "Eco-Lodge"), the location-based ranges remain valid as starting points, and AI research should be re-run to refine for the new asset class.
server/researchSeeds.ts
interface ResearchValueEntry {
display: string; // Human-readable range: "$280–$450", "1.8%–3.5%"
mid: number; // Midpoint value for click-to-apply: 350, 2.5
source: 'seed' | 'ai' | 'none'; // Provenance tracking
}
type ResearchValueMap = Record<string, ResearchValueEntry>;
Stored in: properties.research_values (JSONB column in properties table)
interface LocationContext {
location: string; // e.g., "Upstate New York"
streetAddress?: string; // e.g., "47 Ridgeview Lane, Rhinebeck, NY 12572"
city?: string;
stateProvince?: string;
zipPostalCode?: string;
country?: string;
market: string; // e.g., "North America", "Latin America"
}
regionFromLocation(ctx) concatenates all location fields and pattern-matches against regex rules:
| Region Key | Pattern Examples | Market |
|---|---|---|
ny_metro | new york, nyc, rhinebeck, hudson valley | North America |
south_florida | miami, fort lauderdale, palm beach, key west | North America |
california | los angeles, san francisco, napa, sonoma | North America |
texas | austin, dallas, houston, san antonio | North America |
southeast_resort | asheville, charleston, savannah, nashville | North America |
mountain_west | eden, park city, utah, salt lake, moab | North America |
hawaii | maui, oahu, kauai, big island | North America |
midwest | chicago, detroit, minneapolis, ohio, wisconsin | North America |
new_england | boston, maine, vermont, connecticut | North America |
pacific_northwest | seattle, portland, oregon, washington | North America |
colorado | aspen, vail, denver, telluride | North America |
arizona | scottsdale, sedona, phoenix | North America |
colombia | medellín, bogotá, cartagena | Latin America |
mexico | cancun, tulum, cabo, oaxaca | Latin America |
costa_rica | guanacaste, monteverde, la fortuna | Latin America |
central_america | panama, belize, guatemala | Latin America |
brazil | rio, são paulo, bahia | Latin America |
argentina | buenos aires, mendoza, patagonia | Latin America |
caribbean | jamaica, bahamas, barbados, turks & caicos | Latin America |
europe | london, paris, rome, barcelona, lisbon | Europe |
latam_generic | Fallback when market = "Latin America" | Latin America |
us_generic | Fallback for unmatched US locations | North America |
Each profile is a record of 25 keys, each a [low, mid, high] triple:
type RegionProfile = {
adr: [number, number, number]; // e.g., [280, 350, 450]
occupancy: [number, number, number];
startOccupancy: [number, number, number];
rampMonths: [number, number, number];
capRate: [number, number, number];
catering: [number, number, number];
landValue: [number, number, number];
costHousekeeping: [number, number, number];
costFB: [number, number, number];
costAdmin: [number, number, number];
costPropertyOps: [number, number, number];
costUtilities: [number, number, number];
costFFE: [number, number, number];
costMarketing: [number, number, number];
costIT: [number, number, number];
costOther: [number, number, number];
costInsurance: [number, number, number];
costPropertyTaxes: [number, number, number];
svcFeeMarketing: [number, number, number];
svcFeeIT: [number, number, number];
svcFeeAccounting: [number, number, number];
svcFeeReservations: [number, number, number];
svcFeeGeneralMgmt: [number, number, number];
incentiveFee: [number, number, number];
incomeTax: [number, number, number];
};
US_BASE profile (national averages)applyRegionOverrides(base, region)$low–$high, ramp as low–high mo, everything else as low%–high%| Field | Format | Example |
|---|---|---|
adr | $low–$high | $280–$450 |
rampMonths | low–high mo | 12–24 mo |
| All others | low%–high% | 1.8%–3.5% |
server/routes.ts)app.post("/api/properties", async (req, res) => {
const data = validation.data;
if (!data.researchValues) {
data.researchValues = generateLocationAwareResearchValues({
location: data.location,
streetAddress: data.streetAddress,
city: data.city,
stateProvince: data.stateProvince,
market: data.market,
});
}
const property = await storage.createProperty({ ...data, userId: req.user!.id });
});
During POST /api/admin/seed-data, existing properties without researchValues are backfilled:
for (const existing of existingProperties) {
if (!existing.researchValues) {
const rv = generateLocationAwareResearchValues({ ...existing });
await storage.updateProperty(existing.id, { researchValues: rv });
}
}
regionFromLocation() in server/researchSeeds.tsapplyRegionOverrides() with the fields that differ from US_BASEnpx tsx -e "import { generateLocationAwareResearchValues } from './server/researchSeeds'; console.log(generateLocationAwareResearchValues({ location: 'Your Location', market: 'Your Market' }));"source: 'seed' — never 'ai' or 'none'mid value is what gets applied when a user clicks the ResearchBadgeus_generic is used (national averages)market contains "Latin America" and no specific country matches, latam_generic is used