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
|
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
|
if (need.assetType && prop.assetType !== need.assetType) return null
|
||||||
|
|
||||||
const locScore = locationScore(prop.location.city, need.preferredLocations ?? [])
|
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
|
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 (need.budgetRange?.maxPerSqm && prop.rentPricePerSqm) {
|
||||||
if (prop.rentPricePerSqm <= need.budgetRange.maxPerSqm) score += 10
|
const monthlyRate = prop.rentPricePerSqm / 12
|
||||||
else if (prop.rentPricePerSqm <= need.budgetRange.maxPerSqm * 1.2) score += 3
|
if (monthlyRate <= need.budgetRange.maxPerSqm) score += 10
|
||||||
|
else if (monthlyRate <= need.budgetRange.maxPerSqm * 1.2) score += 3
|
||||||
else score -= 8
|
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
|
// Small jitter so results look natural
|
||||||
score += Math.floor(Math.random() * 6) - 2
|
score += Math.floor(Math.random() * 6) - 2
|
||||||
|
|
||||||
@@ -80,12 +95,19 @@ function generateSyntheticMatches(need: Need) {
|
|||||||
const locS = locationScore(prop.location.city, need.preferredLocations ?? [])
|
const locS = locationScore(prop.location.city, need.preferredLocations ?? [])
|
||||||
const isGoodLoc = locS >= 0.9
|
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 = {
|
const match: Match = {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
propertyId: prop.id,
|
propertyId: prop.id,
|
||||||
needId: need.id,
|
needId: need.id,
|
||||||
resultId: prop.id,
|
resultId: prop.id,
|
||||||
resultType: prop.resultType ?? 'VERIFIED_PORTFOLIO',
|
resultType: effectiveResultType as Match['resultType'],
|
||||||
matchScore: score,
|
matchScore: score,
|
||||||
matchStrength: strengthFromScore(score) as typeof MatchStrength[keyof typeof MatchStrength],
|
matchStrength: strengthFromScore(score) as typeof MatchStrength[keyof typeof MatchStrength],
|
||||||
status: score >= 75 ? MatchStatus.PENDING_REVIEW : MatchStatus.PENDING_REVIEW,
|
status: score >= 75 ? MatchStatus.PENDING_REVIEW : MatchStatus.PENDING_REVIEW,
|
||||||
|
|||||||
Reference in New Issue
Block a user