Improving parameters

This commit is contained in:
Nitzan Pomerantz
2025-07-14 13:12:44 +03:00
parent eb4bf3894c
commit 80dfaa28f0
2 changed files with 410 additions and 93 deletions
+289 -69
View File
@@ -112,46 +112,79 @@ def get_street_deals(polygon_id: str, limit: int = 100) -> str:
return f"Error fetching street deals: {str(e)}"
@mcp.tool()
def find_recent_deals_for_address(address: str, years_back: int = 2) -> str:
def find_recent_deals_for_address(address: str, years_back: int = 2, radius_meters: int = 30, max_deals: int = 200) -> str:
"""Find recent real estate deals for a specific address.
Args:
address: The address to search for (in Hebrew or English)
years_back: How many years back to search (default: 2)
radius_meters: Search radius in meters from the address (default: 30)
Small radius since street deals cover the entire street anyway
max_deals: Maximum number of deals to return (default: 200)
Returns:
JSON string containing recent real estate deals for the address
"""
try:
deals = client.find_recent_deals_for_address(address, years_back)
deals = client.find_recent_deals_for_address(address, years_back, radius_meters, max_deals)
if not deals:
return f"No deals found for address '{address}'"
# Calculate basic statistics
# Calculate comprehensive statistics
prices = [deal.get("dealAmount", 0) for deal in deals if deal.get("dealAmount")]
areas = [deal.get("assetArea", 0) for deal in deals if deal.get("assetArea")]
price_per_sqm_values = [deal.get("price_per_sqm", 0) for deal in deals if deal.get("price_per_sqm")]
# Separate building, street and neighborhood deals for analysis
building_deals = [deal for deal in deals if deal.get("deal_source") == "same_building"]
street_deals = [deal for deal in deals if deal.get("deal_source") == "street"]
neighborhood_deals = [deal for deal in deals if deal.get("deal_source") == "neighborhood"]
stats = {
"deal_breakdown": {
"total_deals": len(deals),
"same_building_deals": len(building_deals),
"street_deals": len(street_deals),
"neighborhood_deals": len(neighborhood_deals),
"same_building_percentage": round((len(building_deals) / len(deals)) * 100, 1) if deals else 0,
"street_emphasis_percentage": round((len(street_deals) / len(deals)) * 100, 1) if deals else 0,
"neighborhood_percentage": round((len(neighborhood_deals) / len(deals)) * 100, 1) if deals else 0
}
}
stats = {}
if prices:
stats["price_stats"] = {
"average_price": sum(prices) / len(prices),
"average_price": round(sum(prices) / len(prices), 0),
"min_price": min(prices),
"max_price": max(prices),
"total_deals": len(prices)
"median_price": sorted(prices)[len(prices)//2] if prices else 0,
"total_volume": sum(prices)
}
if areas:
stats["area_stats"] = {
"average_area": sum(areas) / len(areas),
"average_area": round(sum(areas) / len(areas), 1),
"min_area": min(areas),
"max_area": max(areas)
"max_area": max(areas),
"median_area": sorted(areas)[len(areas)//2] if areas else 0
}
if price_per_sqm_values:
stats["price_per_sqm_stats"] = {
"average_price_per_sqm": round(sum(price_per_sqm_values) / len(price_per_sqm_values), 0),
"min_price_per_sqm": round(min(price_per_sqm_values), 0),
"max_price_per_sqm": round(max(price_per_sqm_values), 0),
"median_price_per_sqm": round(sorted(price_per_sqm_values)[len(price_per_sqm_values)//2], 0) if price_per_sqm_values else 0
}
return json.dumps({
"search_address": address,
"years_back": years_back,
"total_deals": len(deals),
"search_parameters": {
"address": address,
"years_back": years_back,
"radius_meters": radius_meters,
"max_deals": max_deals
},
"market_statistics": stats,
"deals": deals
}, ensure_ascii=False, indent=2)
@@ -177,9 +210,31 @@ def get_neighborhood_deals(polygon_id: str, limit: int = 100) -> str:
if not deals:
return f"No deals found for polygon ID {polygon_id}"
# Add price per sqm calculation for each deal
for deal in deals:
price = deal.get('dealAmount', 0)
area = deal.get('assetArea', 0)
if isinstance(price, (int, float)) and isinstance(area, (int, float)) and area > 0:
deal['price_per_sqm'] = round(price / area, 2)
else:
deal['price_per_sqm'] = None
# Calculate basic statistics including price per sqm
prices = [deal.get("dealAmount", 0) for deal in deals if deal.get("dealAmount")]
price_per_sqm_values = [deal.get("price_per_sqm", 0) for deal in deals if deal.get("price_per_sqm")]
stats = {}
if price_per_sqm_values:
stats["price_per_sqm_stats"] = {
"average_price_per_sqm": round(sum(price_per_sqm_values) / len(price_per_sqm_values), 0),
"min_price_per_sqm": round(min(price_per_sqm_values), 0),
"max_price_per_sqm": round(max(price_per_sqm_values), 0)
}
return json.dumps({
"total_deals": len(deals),
"polygon_id": polygon_id,
"market_statistics": stats,
"deals": deals
}, ensure_ascii=False, indent=2)
@@ -188,94 +243,225 @@ def get_neighborhood_deals(polygon_id: str, limit: int = 100) -> str:
return f"Error fetching neighborhood deals: {str(e)}"
@mcp.tool()
def analyze_market_trends(address: str, years_back: int = 3) -> str:
"""Analyze market trends and price patterns for an area.
def analyze_market_trends(address: str, years_back: int = 3, radius_meters: int = 300, max_deals: int = 500) -> str:
"""Analyze market trends and price patterns for an area with comprehensive data.
Args:
address: The address to analyze trends around
years_back: How many years of data to analyze (default: 3)
radius_meters: Search radius in meters from the address (default: 300, larger for trend analysis)
max_deals: Maximum number of deals to analyze (default: 500)
Returns:
JSON string containing market trend analysis including:
- Price trends over time
JSON string containing comprehensive market trend analysis including:
- Detailed price trends over time
- Average prices by property type
- Market activity levels
- Market activity levels and patterns
- Price per square meter trends
- Seasonal patterns
- Market velocity indicators
- Comparative neighborhood analysis
"""
try:
# Get deals for the address
deals = client.find_recent_deals_for_address(address, years_back)
# Get deals for the address with larger radius for trend analysis
deals = client.find_recent_deals_for_address(address, years_back, radius_meters, max_deals)
if not deals:
return f"No deals found for market analysis near '{address}'"
return f"No deals found for comprehensive market analysis near '{address}'"
# Analyze trends by year
# Comprehensive analysis structure
from collections import defaultdict
yearly_data = defaultdict(list)
property_types: Dict[str, int] = defaultdict(int)
neighborhoods = set()
monthly_data = defaultdict(list)
property_types: Dict[str, List[Dict]] = defaultdict(list)
neighborhoods = defaultdict(list)
quarterly_data = defaultdict(list)
# Process each deal for comprehensive analysis
for deal in deals:
date_str = deal.get('dealDate', '')
if date_str:
year = date_str[:4]
price = deal.get('dealAmount')
area = deal.get('assetArea')
prop_type = deal.get('assetTypeHeb', deal.get('propertyTypeDescription', 'Unknown'))
neighborhood = deal.get('settlementNameHeb', deal.get('neighborhood'))
if not date_str:
continue
if neighborhood:
neighborhoods.add(neighborhood)
year = date_str[:4]
month = date_str[:7] # YYYY-MM
quarter = f"{year}-Q{((int(date_str[5:7]) - 1) // 3) + 1}" if len(date_str) >= 7 else None
if isinstance(price, (int, float)) and isinstance(area, (int, float)) and area > 0:
yearly_data[year].append({
'price': price,
'area': area,
'price_per_sqm': price / area
})
property_types[prop_type] += 1
price = deal.get('dealAmount')
area = deal.get('assetArea')
price_per_sqm = deal.get('price_per_sqm')
prop_type = deal.get('assetTypeHeb', deal.get('propertyTypeDescription', 'לא ידוע'))
neighborhood = deal.get('settlementNameHeb', deal.get('neighborhood', 'לא ידוע'))
deal_source = deal.get('deal_source', 'unknown')
# Calculate yearly trends
deal_data = {
'price': price,
'area': area,
'price_per_sqm': price_per_sqm,
'property_type': prop_type,
'neighborhood': neighborhood,
'deal_source': deal_source,
'date': date_str
}
if isinstance(price, (int, float)) and isinstance(area, (int, float)) and area > 0:
yearly_data[year].append(deal_data)
monthly_data[month].append(deal_data)
if quarter:
quarterly_data[quarter].append(deal_data)
property_types[prop_type].append(deal_data)
neighborhoods[neighborhood].append(deal_data)
# Calculate comprehensive yearly trends
yearly_trends = {}
for year, year_deals in yearly_data.items():
if year_deals:
prices = [d['price'] for d in year_deals if d['price']]
price_per_sqm_vals = [d['price_per_sqm'] for d in year_deals if d['price_per_sqm']]
areas = [d['area'] for d in year_deals if d['area']]
building_deals = [d for d in year_deals if d['deal_source'] == 'same_building']
street_deals = [d for d in year_deals if d['deal_source'] == 'street']
neighborhood_deals = [d for d in year_deals if d['deal_source'] == 'neighborhood']
yearly_trends[year] = {
"average_price": sum(d['price'] for d in year_deals) / len(year_deals),
"min_price": min(d['price'] for d in year_deals),
"max_price": max(d['price'] for d in year_deals),
"average_area": sum(d['area'] for d in year_deals) / len(year_deals),
"average_price_per_sqm": sum(d['price_per_sqm'] for d in year_deals) / len(year_deals),
"deal_count": len(year_deals)
"deal_count": len(year_deals),
"same_building_deals_count": len(building_deals),
"street_deals_count": len(street_deals),
"neighborhood_deals_count": len(neighborhood_deals),
"same_building_percentage": round((len(building_deals) / len(year_deals)) * 100, 1),
"street_deals_percentage": round((len(street_deals) / len(year_deals)) * 100, 1),
"average_price": round(sum(prices) / len(prices), 0) if prices else 0,
"median_price": round(sorted(prices)[len(prices)//2], 0) if prices else 0,
"min_price": min(prices) if prices else 0,
"max_price": max(prices) if prices else 0,
"price_std_dev": round((sum([(p - sum(prices)/len(prices))**2 for p in prices]) / len(prices))**0.5, 0) if len(prices) > 1 else 0,
"average_area": round(sum(areas) / len(areas), 1) if areas else 0,
"average_price_per_sqm": round(sum(price_per_sqm_vals) / len(price_per_sqm_vals), 0) if price_per_sqm_vals else 0,
"median_price_per_sqm": round(sorted(price_per_sqm_vals)[len(price_per_sqm_vals)//2], 0) if price_per_sqm_vals else 0,
"total_market_volume": sum(prices) if prices else 0
}
# Calculate price trend direction
# Calculate quarterly trends for seasonality analysis
quarterly_trends = {}
for quarter, quarter_deals in quarterly_data.items():
if quarter_deals:
prices = [d['price'] for d in quarter_deals if d['price']]
price_per_sqm_vals = [d['price_per_sqm'] for d in quarter_deals if d['price_per_sqm']]
quarterly_trends[quarter] = {
"deal_count": len(quarter_deals),
"average_price": round(sum(prices) / len(prices), 0) if prices else 0,
"average_price_per_sqm": round(sum(price_per_sqm_vals) / len(price_per_sqm_vals), 0) if price_per_sqm_vals else 0
}
# Property type analysis
property_type_analysis = {}
for prop_type, type_deals in property_types.items():
if type_deals:
prices = [d['price'] for d in type_deals if d['price']]
price_per_sqm_vals = [d['price_per_sqm'] for d in type_deals if d['price_per_sqm']]
areas = [d['area'] for d in type_deals if d['area']]
property_type_analysis[prop_type] = {
"deal_count": len(type_deals),
"market_share_percentage": round((len(type_deals) / len(deals)) * 100, 1),
"average_price": round(sum(prices) / len(prices), 0) if prices else 0,
"average_area": round(sum(areas) / len(areas), 1) if areas else 0,
"average_price_per_sqm": round(sum(price_per_sqm_vals) / len(price_per_sqm_vals), 0) if price_per_sqm_vals else 0
}
# Neighborhood comparison analysis
neighborhood_analysis = {}
for neighborhood, neighborhood_deals in neighborhoods.items():
if neighborhood_deals and len(neighborhood_deals) >= 3: # Only include neighborhoods with sufficient data
prices = [d['price'] for d in neighborhood_deals if d['price']]
price_per_sqm_vals = [d['price_per_sqm'] for d in neighborhood_deals if d['price_per_sqm']]
neighborhood_analysis[neighborhood] = {
"deal_count": len(neighborhood_deals),
"average_price": round(sum(prices) / len(prices), 0) if prices else 0,
"average_price_per_sqm": round(sum(price_per_sqm_vals) / len(price_per_sqm_vals), 0) if price_per_sqm_vals else 0
}
# Market trend direction analysis
price_trend_analysis = {}
years_sorted = sorted(yearly_trends.keys())
if len(years_sorted) >= 2:
first_year_avg = yearly_trends[years_sorted[0]]['average_price_per_sqm']
last_year_avg = yearly_trends[years_sorted[-1]]['average_price_per_sqm']
first_year_data = yearly_trends[years_sorted[0]]
last_year_data = yearly_trends[years_sorted[-1]]
trend_percentage = ((last_year_avg - first_year_avg) / first_year_avg) * 100
trend_direction = "rising" if trend_percentage > 5 else "declining" if trend_percentage < -5 else "stable"
# Price trend
first_year_avg = first_year_data['average_price_per_sqm']
last_year_avg = last_year_data['average_price_per_sqm']
price_trend_analysis = {
"trend_direction": trend_direction,
"trend_percentage": round(trend_percentage, 1),
"first_year": years_sorted[0],
"last_year": years_sorted[-1],
"first_year_avg_price_per_sqm": round(first_year_avg, 0),
"last_year_avg_price_per_sqm": round(last_year_avg, 0)
if first_year_avg > 0:
price_trend_percentage = ((last_year_avg - first_year_avg) / first_year_avg) * 100
price_trend_direction = "עולה" if price_trend_percentage > 5 else "יורד" if price_trend_percentage < -5 else "יציב"
# Volume trend
first_year_volume = first_year_data['deal_count']
last_year_volume = last_year_data['deal_count']
volume_trend_percentage = ((last_year_volume - first_year_volume) / first_year_volume) * 100 if first_year_volume > 0 else 0
volume_trend_direction = "עולה" if volume_trend_percentage > 10 else "יורד" if volume_trend_percentage < -10 else "יציב"
price_trend_analysis = {
"price_trend_direction": price_trend_direction,
"price_trend_percentage": round(price_trend_percentage, 1),
"volume_trend_direction": volume_trend_direction,
"volume_trend_percentage": round(volume_trend_percentage, 1),
"analysis_period": f"{years_sorted[0]} - {years_sorted[-1]}",
"first_year_avg_price_per_sqm": round(first_year_avg, 0),
"last_year_avg_price_per_sqm": round(last_year_avg, 0),
"total_price_change": round(last_year_avg - first_year_avg, 0),
"annualized_price_growth": round(price_trend_percentage / len(years_sorted), 1)
}
# Market velocity indicators
market_velocity = {
"average_deals_per_month": round(len(deals) / (years_back * 12), 1),
"peak_activity_quarter": max(quarterly_trends.keys(), key=lambda q: quarterly_trends[q]['deal_count']) if quarterly_trends else None,
"lowest_activity_quarter": min(quarterly_trends.keys(), key=lambda q: quarterly_trends[q]['deal_count']) if quarterly_trends else None
}
# Price distribution analysis
all_prices_per_sqm = [deal.get('price_per_sqm', 0) for deal in deals if deal.get('price_per_sqm')]
price_distribution = {}
if all_prices_per_sqm:
sorted_prices = sorted(all_prices_per_sqm)
price_distribution = {
"25th_percentile": round(sorted_prices[len(sorted_prices)//4], 0),
"75th_percentile": round(sorted_prices[3*len(sorted_prices)//4], 0),
"price_range_iqr": round(sorted_prices[3*len(sorted_prices)//4] - sorted_prices[len(sorted_prices)//4], 0),
"coefficient_of_variation": round((yearly_trends[years_sorted[-1]]['price_std_dev'] / yearly_trends[years_sorted[-1]]['average_price']) * 100, 1) if years_sorted and yearly_trends[years_sorted[-1]]['average_price'] > 0 else 0
}
return json.dumps({
"analysis_address": address,
"analysis_period_years": years_back,
"total_deals_analyzed": len(deals),
"neighborhoods": list(neighborhoods),
"property_types": dict(property_types),
"analysis_parameters": {
"address": address,
"analysis_period_years": years_back,
"search_radius_meters": radius_meters,
"max_deals_analyzed": max_deals
},
"market_overview": {
"total_deals_analyzed": len(deals),
"unique_neighborhoods": len(neighborhoods),
"unique_property_types": len(property_types),
"data_coverage_years": len(yearly_trends)
},
"yearly_trends": yearly_trends,
"price_trend_analysis": price_trend_analysis
"quarterly_trends": quarterly_trends,
"property_type_analysis": property_type_analysis,
"neighborhood_comparison": neighborhood_analysis,
"market_trend_analysis": price_trend_analysis,
"market_velocity_indicators": market_velocity,
"price_distribution_analysis": price_distribution,
"detailed_insights": {
"most_active_property_type": max(property_type_analysis.keys(), key=lambda pt: property_type_analysis[pt]['deal_count']) if property_type_analysis else None,
"highest_value_property_type": max(property_type_analysis.keys(), key=lambda pt: property_type_analysis[pt]['average_price_per_sqm']) if property_type_analysis else None,
"most_expensive_neighborhood": max(neighborhood_analysis.keys(), key=lambda n: neighborhood_analysis[n]['average_price_per_sqm']) if neighborhood_analysis else None,
"deal_source_breakdown": f"Same building: {len([d for d in deals if d.get('deal_source') == 'same_building'])}, Street: {len([d for d in deals if d.get('deal_source') == 'street'])}, Neighborhood: {len([d for d in deals if d.get('deal_source') == 'neighborhood'])}"
}
}, ensure_ascii=False, indent=2)
except Exception as e:
@@ -302,27 +488,47 @@ def compare_addresses(addresses: List[str]) -> str:
if deals:
prices = [deal.get("dealAmount", 0) for deal in deals if deal.get("dealAmount")]
areas = [deal.get("assetArea", 0) for deal in deals if deal.get("assetArea")]
price_per_sqm_values = [deal.get("price_per_sqm", 0) for deal in deals if deal.get("price_per_sqm")]
building_deals = [deal for deal in deals if deal.get("deal_source") == "same_building"]
street_deals = [deal for deal in deals if deal.get("deal_source") == "street"]
neighborhood_deals = [deal for deal in deals if deal.get("deal_source") == "neighborhood"]
comparison = {
"address": address,
"total_deals": len(deals),
"same_building_deals": len(building_deals),
"street_deals": len(street_deals),
"neighborhood_deals": len(neighborhood_deals),
"same_building_percentage": round((len(building_deals) / len(deals)) * 100, 1) if deals else 0,
"street_emphasis_percentage": round((len(street_deals) / len(deals)) * 100, 1) if deals else 0,
"price_stats": {
"average_price": sum(prices) / len(prices) if prices else 0,
"average_price": round(sum(prices) / len(prices), 0) if prices else 0,
"min_price": min(prices) if prices else 0,
"max_price": max(prices) if prices else 0
},
"area_stats": {
"average_area": sum(areas) / len(areas) if areas else 0,
"average_area": round(sum(areas) / len(areas), 1) if areas else 0,
"min_area": min(areas) if areas else 0,
"max_area": max(areas) if areas else 0
},
"price_per_sqm_stats": {
"average_price_per_sqm": round(sum(price_per_sqm_values) / len(price_per_sqm_values), 0) if price_per_sqm_values else 0,
"min_price_per_sqm": round(min(price_per_sqm_values), 0) if price_per_sqm_values else 0,
"max_price_per_sqm": round(max(price_per_sqm_values), 0) if price_per_sqm_values else 0
}
}
else:
comparison = {
"address": address,
"total_deals": 0,
"same_building_deals": 0,
"street_deals": 0,
"neighborhood_deals": 0,
"same_building_percentage": 0,
"street_emphasis_percentage": 0,
"price_stats": {},
"area_stats": {}
"area_stats": {},
"price_per_sqm_stats": {}
}
comparisons.append(comparison)
@@ -334,13 +540,27 @@ def compare_addresses(addresses: List[str]) -> str:
"error": str(e)
})
# Rank addresses by average price
valid_comparisons = [c for c in comparisons if c.get("price_stats", {}).get("average_price", 0) > 0]
valid_comparisons.sort(key=lambda x: x["price_stats"]["average_price"], reverse=True)
# Rank addresses by average price per sqm
valid_comparisons = []
for comparison in comparisons:
if (isinstance(comparison, dict) and
"price_per_sqm_stats" in comparison and
isinstance(comparison["price_per_sqm_stats"], dict) and
comparison["price_per_sqm_stats"].get("average_price_per_sqm", 0) > 0):
valid_comparisons.append(comparison)
# Sort by price per sqm
def get_price_per_sqm(comp: dict) -> float:
price_stats = comp.get("price_per_sqm_stats", {})
if isinstance(price_stats, dict):
return price_stats.get("average_price_per_sqm", 0)
return 0
valid_comparisons.sort(key=get_price_per_sqm, reverse=True)
return json.dumps({
"addresses_compared": len(addresses),
"ranking_by_average_price": valid_comparisons,
"ranking_by_average_price_per_sqm": valid_comparisons,
"all_results": comparisons
}, ensure_ascii=False, indent=2)
+119 -22
View File
@@ -225,18 +225,24 @@ class GovmapClient:
logger.error(f"Error parsing JSON response: {e}")
return []
def find_recent_deals_for_address(self, address: str, years_back: int = 2) -> List[Dict[str, Any]]:
def find_recent_deals_for_address(self, address: str, years_back: int = 2,
radius: int = 30, max_deals: int = 200) -> List[Dict[str, Any]]:
"""
Find all relevant real estate deals for a given address from the last few years.
This is the main use case function that ties everything together.
Street deals include deals from the same building which get highest priority.
Args:
address: The address to search for
years_back: How many years back to search (default: 2)
radius: Search radius in meters for initial coordinate search (default: 30)
Small radius since street deals cover the entire street anyway
max_deals: Maximum number of deals to return (default: 200)
Returns:
List of deals found for the address area
List of deals found for the address area, with same building deals prioritized first,
then street deals, then neighborhood deals
Raises:
ValueError: If address cannot be found or processed
@@ -268,10 +274,11 @@ class GovmapClient:
raise ValueError("Invalid coordinate format in autocomplete result")
point = (float(coords[0]), float(coords[1]))
search_address_normalized = address.lower().strip()
logger.info(f"Found coordinates: {point}")
# Step 2: Get deals by radius to find polygon IDs
nearby_deals = self.get_deals_by_radius(point, radius=30) # Slightly larger radius
nearby_deals = self.get_deals_by_radius(point, radius=radius)
# Extract unique polygon IDs
polygon_ids = set()
@@ -288,46 +295,136 @@ class GovmapClient:
end_date_str = end_date.strftime('%Y-%m')
# Step 4: Get street and neighborhood deals for each polygon
all_deals = []
# Prioritize: same building (0) > street deals (1) > neighborhood deals (2)
building_deals = []
street_deals = []
neighborhood_deals = []
seen_deals = set() # For deduplication
for polygon_id in polygon_ids:
try:
# Get street deals
street_deals = self.get_street_deals(
polygon_id, limit=50,
# Get street deals first (higher priority)
current_street_deals = self.get_street_deals(
polygon_id, limit=max_deals // 2, # Allocate more to street deals
start_date=start_date_str, end_date=end_date_str
)
# Get neighborhood deals
neighborhood_deals = self.get_neighborhood_deals(
polygon_id, limit=50,
# Get neighborhood deals (lower priority)
current_neighborhood_deals = self.get_neighborhood_deals(
polygon_id, limit=max_deals // 4, # Allocate less to neighborhood deals
start_date=start_date_str, end_date=end_date_str
)
# Combine deals
combined_deals = street_deals + neighborhood_deals
# Add to results with deduplication
for deal in combined_deals:
# Create a unique identifier for the deal
# Process street deals and separate building deals
for deal in current_street_deals:
deal_id = f"{deal.get('dealId', '')}{deal.get('address', '')}{deal.get('dealDate', '')}"
if deal_id not in seen_deals:
seen_deals.add(deal_id)
deal['source_polygon_id'] = polygon_id # Add source for reference
all_deals.append(deal)
deal['source_polygon_id'] = polygon_id
deal['deal_source'] = 'street'
# Check if this is from the same building
deal_address = deal.get('address', '').lower().strip()
if self._is_same_building(search_address_normalized, deal_address):
deal['deal_source'] = 'same_building'
deal['priority'] = 0 # Highest priority
building_deals.append(deal)
else:
deal['priority'] = 1 # Street deals priority
street_deals.append(deal)
# Add neighborhood deals with lowest priority
for deal in current_neighborhood_deals:
deal_id = f"{deal.get('dealId', '')}{deal.get('address', '')}{deal.get('dealDate', '')}"
if deal_id not in seen_deals:
seen_deals.add(deal_id)
deal['source_polygon_id'] = polygon_id
deal['deal_source'] = 'neighborhood'
deal['priority'] = 2 # Lowest priority
neighborhood_deals.append(deal)
except Exception as e:
logger.warning(f"Error processing polygon {polygon_id}: {e}")
continue
# Step 5: Sort by date (newest first)
all_deals.sort(key=lambda x: x.get('dealDate', ''), reverse=True)
# Step 5: Combine and prioritize: building deals first, then street, then neighborhood
all_deals = building_deals + street_deals + neighborhood_deals
logger.info(f"Found {len(all_deals)} total deals for address: {address}")
# Use stable sort: first by date (newest first), then by priority
# Since Python's sort is stable, the second sort maintains date order within each priority
all_deals.sort(key=lambda x: x.get('dealDate', '1900-01-01'), reverse=True) # Newest first
all_deals.sort(key=lambda x: x.get('priority', 3)) # Priority first (0=building, 1=street, 2=neighborhood)
# Limit to max_deals
if len(all_deals) > max_deals:
all_deals = all_deals[:max_deals]
# Add price per square meter calculation
for deal in all_deals:
price = deal.get('dealAmount', 0)
area = deal.get('assetArea', 0)
if isinstance(price, (int, float)) and isinstance(area, (int, float)) and area > 0:
deal['price_per_sqm'] = round(price / area, 2)
else:
deal['price_per_sqm'] = None
logger.info(f"Found {len(all_deals)} total deals for address: {address} "
f"(Building: {len(building_deals)}, Street: {len(street_deals)}, Neighborhood: {len(neighborhood_deals)})")
return all_deals
except Exception as e:
logger.error(f"Error in find_recent_deals_for_address: {e}")
raise
def _is_same_building(self, search_address: str, deal_address: str) -> bool:
"""
Check if a deal is from the same building as the search address.
Args:
search_address: The normalized search address (lowercase, stripped)
deal_address: The normalized deal address (lowercase, stripped)
Returns:
True if likely the same building, False otherwise
"""
if not search_address or not deal_address:
return False
# Exact match
if search_address == deal_address:
return True
# Extract key components for comparison
def extract_address_parts(addr: str) -> tuple:
"""Extract street name and number from address"""
# Remove common prefixes/suffixes and normalize
addr_clean = addr.replace('רח\'', '').replace('רחוב', '').replace('שד\'', '').replace('שדרות', '')
addr_clean = addr_clean.replace(' ', ' ').strip()
# Try to extract number and street name
parts = addr_clean.split()
if len(parts) >= 2:
# Look for number (could be at start or end)
for i, part in enumerate(parts):
if part.isdigit() or any(c.isdigit() for c in part):
number = part
street_parts = parts[:i] + parts[i+1:]
street_name = ' '.join(street_parts).strip()
return (street_name, number)
return (addr_clean, '')
search_street, search_number = extract_address_parts(search_address)
deal_street, deal_number = extract_address_parts(deal_address)
# Same street and same number = same building
if (search_street and deal_street and search_number and deal_number and
search_street == deal_street and search_number == deal_number):
return True
# Check if one address is contained in the other (for different formats of same address)
if len(search_address) > 5 and len(deal_address) > 5:
if search_address in deal_address or deal_address in search_address:
return True
return False