Feature: Search
Property search is a UK-only feature that queries the MySQL database directly (not the REST API). All search logic lives in src/features/search/.
Directory Structure
src/features/search/
actions/
get-properties.ts # Main property search query
get-area-property-counts.ts # Area-level property count queries
api/
components/
hooks/
types/
utils/How Search Works
Direct MySQL Queries
Search bypasses the REST API and queries the houses table directly via src/lib/db.ts. This is a server action (runs server-side only).
The main query in get-properties.ts:
- Builds a
WHEREclause from search params (city, bedrooms, bathrooms, map bounds) - JOINs with
bills_prices_selectable(from the bills database) to calculate total price including bills - JOINs with
bills_included_agentsto account for agents that include certain bills - Applies
HAVINGconditions for price range filters (since total price is a calculated column) - Orders results based on sort preference
- Paginates with
LIMIT/OFFSET
Search Parameters
type SearchParams = {
city: string;
page?: string;
minBedrooms?: string;
maxBedrooms?: string;
minBathrooms?: string;
maxBathrooms?: string;
minPrice?: string;
maxPrice?: string;
sort?: string; // "recommended" | "price-low" | "price-high" | "newest" | "oldest"
limit?: number; // default: 24
bounds?: { // map viewport bounds
west: number;
south: number;
east: number;
north: number;
};
};Sort Options
| Sort Value | SQL Order |
|---|---|
recommended (default) | tier ASC, house_priority DESC, RAND() |
price-low | total_price ASC |
price-high | total_price DESC |
newest | created_at DESC |
oldest | created_at ASC |
Total Price Calculation
Total price = rent per week + bills cost. The bills cost is calculated in SQL by joining with the bills pricing table and accounting for which utilities the agent already includes:
- If agent includes both electric and gas, energy cost = 0
- If agent includes only gas, energy cost = electric only
- If agent includes only electric, energy cost = total energy minus electric
- Water and WiFi are added unless the agent includes them
Caching Strategy
Two unstable_cache wrappers handle caching:
getCachedProperties(city)— Used when no filters are applied. Cache key:["properties"], revalidation: 3 hours.getCachedPropertiesWithFilters(...)— Used when filters are present. Cache key:["properties-filtered"], revalidation: 3 hours.
Both share the "properties" cache tag for on-demand revalidation.
Map-bounds queries are never cached — when bounds is provided, fetchPropertiesFromDb() is called directly.
Area Property Counts
get-area-property-counts.ts provides getAreaPropertyCounts(city, areas):
- Takes a city name and an array of areas with bounding boxes
- Runs parallel
COUNT(*)queries for each area - Results are cached via
unstable_cachewith the same 3-hour TTL and"properties"tag
Response Shape
type PropertiesResponse = {
houses: House[];
totalHouses: number;
totalPages: number;
currentPage: number;
filters: Filters;
};Each House object includes id, title, address, city, images (parsed from JSON), price_pw, total_price (rent + bills), bedrooms, bathrooms, lat, lng, tier, and timestamps.