fix: synthetic scoring — budget unit mismatch + PRE-MARKET VERIFIED in demand feed

Two root causes prevented gold and PRE-MARKET VERIFIED cards from showing
in wizard-created searches:

1. computeScore compared annual property rates (e.g. 1080 CHF/m²/year)
   against monthly AI-extracted budget (e.g. 150 CHF/m²/month), causing
   a constant -8 penalty and capping Bern retail at ~79%. Fix: divide
   rentPricePerSqm by 12 before comparing.

2. generateSyntheticMatches set resultType: VERIFIED_PORTFOLIO for
   schattenmarktRelease-enabled properties, which Results.tsx filters
   out for demand users. Fix: set resultType: FUTURE_AVAILABILITY for
   these properties so the schattenmarkt signal is resolved and the
   PRE-MARKET VERIFIED card is rendered.

Also adds a score discount (×0.82 for probabilistic FUTURE_AVAILABILITY,
×0.92 for PRE-MARKET) to keep tier distribution realistic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-20 19:29:06 +02:00
parent dd2eb9be3f
commit 66aabde1dc
+27 -5
View File
@@ -35,7 +35,14 @@ function locationScore(propCity: string, preferredLocations: string[]): number {
return 0.30
}
function computeScore(prop: { assetType: string; areaSqm: number; rentPricePerSqm: number; location: { city: string } }, need: Need): number | null {
function computeScore(
prop: {
assetType: string; areaSqm: number; rentPricePerSqm: number;
location: { city: string }; resultType?: string;
schattenmarktRelease?: { enabled?: boolean }
},
need: Need,
): number | null {
if (need.assetType && prop.assetType !== need.assetType) return null
const locScore = locationScore(prop.location.city, need.preferredLocations ?? [])
@@ -51,13 +58,21 @@ function computeScore(prop: { assetType: string; areaSqm: number; rentPricePerSq
else if (prop.areaSqm < min * 0.5 || prop.areaSqm > max * 2) score -= 10
}
// Budget fit (+0-10)
// Budget fit (+0-10): property stores annual CHF/m², need budget is monthly CHF/m²
if (need.budgetRange?.maxPerSqm && prop.rentPricePerSqm) {
if (prop.rentPricePerSqm <= need.budgetRange.maxPerSqm) score += 10
else if (prop.rentPricePerSqm <= need.budgetRange.maxPerSqm * 1.2) score += 3
const monthlyRate = prop.rentPricePerSqm / 12
if (monthlyRate <= need.budgetRange.maxPerSqm) score += 10
else if (monthlyRate <= need.budgetRange.maxPerSqm * 1.2) score += 3
else score -= 8
}
// Discount for probabilistic/future signals so they score below confirmed listings
if (prop.resultType === 'FUTURE_AVAILABILITY') {
score = Math.round(score * 0.82)
} else if (prop.schattenmarktRelease?.enabled) {
score = Math.round(score * 0.92)
}
// Small jitter so results look natural
score += Math.floor(Math.random() * 6) - 2
@@ -80,12 +95,19 @@ function generateSyntheticMatches(need: Need) {
const locS = locationScore(prop.location.city, need.preferredLocations ?? [])
const isGoodLoc = locS >= 0.9
// VERIFIED_PORTFOLIO properties with schattenmarktRelease appear as FUTURE_AVAILABILITY
// in demand search so demand users see the PRE-MARKET VERIFIED card
const effectiveResultType =
prop.resultType === 'VERIFIED_PORTFOLIO' && prop.schattenmarktRelease?.enabled
? 'FUTURE_AVAILABILITY'
: (prop.resultType ?? 'VERIFIED_PORTFOLIO')
const match: Match = {
id: crypto.randomUUID(),
propertyId: prop.id,
needId: need.id,
resultId: prop.id,
resultType: prop.resultType ?? 'VERIFIED_PORTFOLIO',
resultType: effectiveResultType as Match['resultType'],
matchScore: score,
matchStrength: strengthFromScore(score) as typeof MatchStrength[keyof typeof MatchStrength],
status: score >= 75 ? MatchStatus.PENDING_REVIEW : MatchStatus.PENDING_REVIEW,