feat: soft factor enrichment from Swiss location intelligence

When property has no soft factor data, estimate from location:
- 18 location rules covering CH cities/districts (Zürich quartiers,
  Zug tax haven, Bern, Basel, Genf, Lausanne, Luzern, Winterthur,
  logistics hubs) + generic CH fallback
- Estimated factors marked with 'estimated: true' on ScoreFactor
- ScoreBreakdownPanel shows purple 'Schätzung' badge on estimated rows
- Scoring engine: neutral 50 only if no location match found

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-22 11:04:51 +02:00
parent f82194ea7f
commit 98b4a1146b
4 changed files with 210 additions and 2 deletions
+19 -1
View File
@@ -13,6 +13,7 @@ import {
import type { ScoringWeightProfile, HardFilterResult, MatchEngineOutput, SoftFactorKey } from '../../domain/scoring'
import { analyzeTradeOffs, analyzeRisks, identifyMissingData } from './tradeOffAnalyzer'
import { generateNextBestActions } from './rankingEngine'
import { softFactorEnrichmentService } from '../../services/softFactorEnrichmentService'
// ── Profile resolution ────────────────────────────────────────────────────────
@@ -264,7 +265,24 @@ function scoreSoftFactor(key: SoftFactorKey, weight: number, property: Property)
})()
if (rawValue === undefined || rawValue === null) {
// Missing data → neutral 50 (does not help, does not hurt)
// No direct data — try location-intelligence estimate
const est = softFactorEnrichmentService.estimate(key, property)
if (est) {
const score = Math.round(Math.min(100, Math.max(0, est.score * 100)))
const LABELS: Record<SoftFactorKey, string> = {
prestige: 'Prestige', accessibility: 'Erreichbarkeit', expansionPotential: 'Expansion',
flexibility: 'Flexibilität', visibility: 'Sichtbarkeit', footfall: 'Passantenfrequenz',
talentAccess: 'Talent-Zugang', esg: 'ESG', taxEnvironment: 'Steuerumfeld',
}
return {
criterion: key,
weight,
score,
contribution: score * weight,
explanation: `${LABELS[key] ?? key}: ${score}/100 — Schätzung basierend auf Standort (${est.label})`,
estimated: true,
}
}
return {
criterion: key,
weight,