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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user