Files
property-match/src/lib/locationIntelligence.ts
T
Benjamin Sutter 09095a1a32 fix(ai): realistic pre-market rent recommendation — annual units + anchor on current price
- getMarketRent now returns CHF/m²/YEAR (stored medians are monthly → ×12); fixes annual-vs-monthly mismatch that also affected MarketPricePanel and NegotiationInsightsPanel
- recommendPreMarketRent: anchor on the unit's current rent and adjust within a bounded ±15% by market momentum (vacancy=supply, demand strength, days-on-market, trend) instead of jumping to the city-wide (prime-skewed) median — no more "double the price" suggestions; median only used as a headroom check
- tests updated to annual market values

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 00:39:37 +02:00

181 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Static location intelligence data per city — mock values based on Swiss market context
export interface CityIntelligence {
vacancyRatePct: number // Leerstandsquote %
rentTrend12m: number // Mietpreisveränderung % (letztes Jahr)
purchasingPowerIndex: number // Kaufkraft-Index (CH = 100)
dominantIndustryClusters: string[]
plannedInfrastructure: { project: string; timeline: string; impact: string }[]
medianRentOffice: number // CHF/m²/Monat (Bürofläche) — getMarketRent rechnet auf Jahr um
medianRentLogistics: number // CHF/m²/Monat
medianRentRetail: number // CHF/m²/Monat
avgDaysOnMarket: number // Durchschnittliche Tage bis Vermietung
demandStrength: 'LOW' | 'MEDIUM' | 'HIGH' | 'VERY_HIGH'
taxIndexCanton: number // Steuerindex 100 = CH-Mittel
taxCalculatorUrl?: string // Kantonaler Steuerrechner
}
export const CITY_INTELLIGENCE: Record<string, CityIntelligence> = {
'Zürich': {
vacancyRatePct: 2.8,
rentTrend12m: +4.2,
purchasingPowerIndex: 128,
dominantIndustryClusters: ['Finanz & Banking', 'Tech & Startups', 'Medien & Kreativ', 'Pharma & Life Science'],
plannedInfrastructure: [
{ project: 'Tram Hardbrücke-Verlängerung', timeline: '2027', impact: 'Bessere ÖV-Anbindung Industriequartier' },
{ project: 'Rosengarten-Tunnel', timeline: '2030', impact: 'Entlastung Kreis 5/6, weniger Durchgangsverkehr' },
],
medianRentOffice: 42,
medianRentLogistics: 14,
medianRentRetail: 180,
avgDaysOnMarket: 38,
demandStrength: 'VERY_HIGH',
taxIndexCanton: 100,
taxCalculatorUrl: 'https://www.zh.ch/de/steuern-finanzen/steuerrechner.html',
},
'Basel': {
vacancyRatePct: 4.1,
rentTrend12m: +1.8,
purchasingPowerIndex: 112,
dominantIndustryClusters: ['Pharma & Chemie', 'Logistik & Handel', 'Medizintechnik', 'Finanzdienstleistungen'],
plannedInfrastructure: [
{ project: 'Basel SBB Südeingang Neubau', timeline: '2026', impact: 'Aufwertung Bahnhofumgebung' },
{ project: 'Regio-S-Bahn Ausbau', timeline: '2028', impact: 'Bessere Grenzpendler-Anbindung' },
],
medianRentOffice: 32,
medianRentLogistics: 11,
medianRentRetail: 120,
avgDaysOnMarket: 52,
demandStrength: 'HIGH',
taxIndexCanton: 98,
taxCalculatorUrl: 'https://www.steuern.bs.ch/steuerrechner',
},
'Bern': {
vacancyRatePct: 3.5,
rentTrend12m: +2.1,
purchasingPowerIndex: 108,
dominantIndustryClusters: ['Bundesverwaltung & NPO', 'Gesundheit', 'Bildung & Forschung', 'Versicherungen'],
plannedInfrastructure: [
{ project: 'Bernmobil Netzausbau West', timeline: '2026', impact: 'Erschliessung Entwicklungsgebiet Ausserholligen' },
],
medianRentOffice: 28,
medianRentLogistics: 10,
medianRentRetail: 95,
avgDaysOnMarket: 61,
demandStrength: 'MEDIUM',
taxIndexCanton: 112,
taxCalculatorUrl: 'https://www.taxme.ch',
},
'Zug': {
vacancyRatePct: 1.9,
rentTrend12m: +5.1,
purchasingPowerIndex: 148,
dominantIndustryClusters: ['Rohstoffhandel', 'Crypto & Blockchain', 'Holding & Finanzen', 'Tech-Unternehmen'],
plannedInfrastructure: [
{ project: 'Metrobahn Zug-Luzern', timeline: '2029', impact: 'Direktverbindung Luzern in 18 Min.' },
],
medianRentOffice: 38,
medianRentLogistics: 13,
medianRentRetail: 140,
avgDaysOnMarket: 24,
demandStrength: 'VERY_HIGH',
taxIndexCanton: 60,
taxCalculatorUrl: 'https://www.zg.ch/behoerden/finanzdirektion/steuerverwaltung/steuerrechner',
},
'Winterthur': {
vacancyRatePct: 5.8,
rentTrend12m: +0.9,
purchasingPowerIndex: 98,
dominantIndustryClusters: ['Industrie & Maschinenbau', 'Logistik', 'Gesundheit & Soziales'],
plannedInfrastructure: [
{ project: 'Stadtraum HB Winterthur', timeline: '2027', impact: 'Aufwertung Bahnhofsumgebung, mehr Frequenz' },
],
medianRentOffice: 22,
medianRentLogistics: 9,
medianRentRetail: 75,
avgDaysOnMarket: 74,
demandStrength: 'MEDIUM',
taxIndexCanton: 119,
taxCalculatorUrl: 'https://www.zh.ch/de/steuern-finanzen/steuerrechner.html',
},
'Geneva': {
vacancyRatePct: 2.2,
rentTrend12m: +3.6,
purchasingPowerIndex: 135,
dominantIndustryClusters: ['Internationale Organisationen', 'Luxusgüter', 'Banking & Private Equity', 'Uhrenindustrie'],
plannedInfrastructure: [
{ project: 'CEVA Linie Verlängerung', timeline: '2026', impact: 'Bessere Verbindung Lancy-Pont-Rouge' },
],
medianRentOffice: 55,
medianRentLogistics: 18,
medianRentRetail: 220,
avgDaysOnMarket: 31,
demandStrength: 'HIGH',
taxIndexCanton: 125,
taxCalculatorUrl: 'https://www.ge.ch/calculer-impots',
},
'St.Gallen': {
vacancyRatePct: 6.2,
rentTrend12m: -0.5,
purchasingPowerIndex: 95,
dominantIndustryClusters: ['Textil & Mode', 'KMU', 'Logistik', 'Gesundheit'],
plannedInfrastructure: [],
medianRentOffice: 19,
medianRentLogistics: 8,
medianRentRetail: 65,
avgDaysOnMarket: 88,
demandStrength: 'LOW',
taxIndexCanton: 107,
taxCalculatorUrl: 'https://www.steuern.sg.ch/home/steuerrechner.html',
},
}
export function getCityIntelligence(city: string): CityIntelligence | null {
// Try exact match first, then partial
if (CITY_INTELLIGENCE[city]) return CITY_INTELLIGENCE[city]
const key = Object.keys(CITY_INTELLIGENCE).find(k => city.toLowerCase().includes(k.toLowerCase()))
return key ? CITY_INTELLIGENCE[key] : null
}
/** Median-Marktmiete in CHF/m²/**Jahr** (Daten sind monatlich gespeichert → ×12), passend zu property.rentPricePerSqm. */
export function getMarketRent(city: string, assetType: string): number | null {
const intel = getCityIntelligence(city)
if (!intel) return null
const monthly =
assetType === 'LOGISTICS' || assetType === 'LIGHT_INDUSTRIAL' ? intel.medianRentLogistics
: assetType === 'RETAIL' ? intel.medianRentRetail
: intel.medianRentOffice
return monthly * 12
}
// ── City coordinates (WGS84) ─────────────────────────────────────────────────
const CITY_COORDS: Record<string, { lat: number; lng: number }> = {
'Zürich': { lat: 47.3769, lng: 8.5417 },
'Zürich-West': { lat: 47.3862, lng: 8.5195 },
'Zürich-Nord': { lat: 47.4108, lng: 8.5450 },
'Basel': { lat: 47.5596, lng: 7.5886 },
'Bern': { lat: 46.9480, lng: 7.4474 },
'Luzern': { lat: 47.0502, lng: 8.3093 },
'Genf': { lat: 46.2044, lng: 6.1432 },
'Lausanne': { lat: 46.5197, lng: 6.6323 },
'Zug': { lat: 47.1661, lng: 8.5158 },
'Winterthur': { lat: 47.5004, lng: 8.7256 },
'St. Gallen': { lat: 47.4245, lng: 9.3767 },
'Lugano': { lat: 46.0037, lng: 8.9511 },
'Baar': { lat: 47.1965, lng: 8.5286 },
'Muttenz': { lat: 47.5218, lng: 7.6468 },
'Pratteln': { lat: 47.5163, lng: 7.6926 },
'Reinach BL': { lat: 47.4948, lng: 7.5946 },
'Allschwil': { lat: 47.5529, lng: 7.5368 },
'Binningen': { lat: 47.5363, lng: 7.5687 },
}
export function lookupCityCoords(location: string): { lat: number; lng: number } | null {
if (CITY_COORDS[location]) return CITY_COORDS[location]
// Prefix fallback: "Zürich Kreis 5" → "Zürich"
const base = location.split(' ')[0]
const key = Object.keys(CITY_COORDS).find(k => k.startsWith(base) || base === k)
return key ? CITY_COORDS[key] : null
}