Weighted match scoring (0–100)
The heart of the marketplace. Each dimension contributes a weighted slice that sums to 100. Price uses tolerance bands — full credit in range, then graduated partial credit just outside it — so strong near-misses still surface instead of being dropped.
// Price match (30%)
// Use budget range if available, otherwise fall back to legacy budget field
const budgetMin = buyer.budget_min ?? Math.round(buyer.budget * 0.8)
const budgetMax = buyer.budget_max ?? buyer.budget
// Full points if listing price is within buyer's budget range
if (listing.asking_price >= budgetMin && listing.asking_price <= budgetMax) {
scores.price_match = 30
} else if (listing.asking_price < budgetMin) {
// Below minimum - partial points (buyer might want something more expensive)
const percentBelow = (budgetMin - listing.asking_price) / budgetMin
if (percentBelow <= 0.2) {
// Within 20% below minimum - scale from 30 down to 20
scores.price_match = Math.round(30 - (percentBelow * 50))
} else {
scores.price_match = 15 // Too far below budget range
}
} else {
// Over budget_max - partial if within 10% over
const percentOver = (listing.asking_price - budgetMax) / budgetMax
if (percentOver <= 0.1) {
// Within 10% over budget - scale from 30 down to 20
scores.price_match = Math.round(30 - (percentOver * 100))
}
// Over 10% = 0 points
}
// Bedrooms (10%)
if (listing.bedrooms >= buyer.min_bedrooms) {
scores.bedrooms = 10
} else if (listing.bedrooms === buyer.min_bedrooms - 1) {
// One less bedroom - partial credit
scores.bedrooms = 5
}
// Bathrooms (10%)
if (listing.bathrooms >= buyer.min_bathrooms) {
scores.bathrooms = 10
} else if (listing.bathrooms >= buyer.min_bathrooms - 0.5) {
scores.bathrooms = 7
} else if (listing.bathrooms >= buyer.min_bathrooms - 1) {
scores.bathrooms = 4
} else if (listing.bathrooms >= buyer.min_bathrooms - 2) {
scores.bathrooms = 2
}
// Location - ZIP code (15%)
if (buyer.zip_codes && buyer.zip_codes.length > 0) {
if (buyer.zip_codes.includes(listing.zip_code)) {
scores.location = 15
}
} else {
scores.location = 15
}
// Property type / Home type (10%)
if (buyer.home_types && buyer.home_types.length > 0) {
if (
buyer.home_types.includes('no_preference') ||
buyer.home_types.includes(listing.property_type)
) {
scores.property_type = 10
}
} else {
// No home types specified = flexible
scores.property_type = 10
}