CREATE OR REPLACE FUNCTION match_pois_hybrid( query_embedding vector(384), match_threshold float DEFAULT 0.5, match_count int DEFAULT 200, alpha_weight float DEFAULT 0.7, beta_weight float DEFAULT 0.3, p_city text DEFAULT NULL, p_neighborhood text DEFAULT NULL, p_main_category text DEFAULT NULL, p_sub_category text DEFAULT NULL, p_cuisine text DEFAULT NULL, p_min_rating float DEFAULT NULL, p_rank int DEFAULT NULL, p_tags text[] DEFAULT ARRAY[]::text[], p_avoid text[] DEFAULT ARRAY[]::text[], p_open_now boolean DEFAULT NULL, p_is_24_7 boolean DEFAULT NULL, p_route_enabled boolean DEFAULT false, p_route_origin text DEFAULT NULL, p_route_stops text[] DEFAULT ARRAY[]::text[], p_route_destination text DEFAULT NULL, p_user_lat float DEFAULT NULL, p_user_lng float DEFAULT NULL, p_proximity_enabled boolean DEFAULT false, p_max_distance_km float DEFAULT NULL, p_is_restaurant_query boolean DEFAULT false, p_is_istanbul_route boolean DEFAULT false, p_use_route_stops boolean DEFAULT false, p_destination_priority boolean DEFAULT false ) RETURNS TABLE ( id uuid, display_title text, city text, neighborhood text, main_category text, sub_category text, cuisine text, total_score float, rank int, tags text[], latitude float, longitude float, is_24_7 boolean, semantic_similarity float, metadata_score float, candidate_score float ) LANGUAGE sql AS $$ WITH base AS ( SELECT p.id, p.display_title, p.city, p.neighborhood, p.main_category, p.sub_category, r.cuisine_type AS cuisine, p.total_score, p.rank, p.tags, p.latitude, p.longitude, p.is_24_7, p.embedding, 1 - (p.embedding <=> query_embedding) AS semantic_similarity, ( CASE WHEN p_min_rating IS NOT NULL AND p.total_score >= p_min_rating THEN 1 ELSE 0 END + CASE WHEN p_rank IS NOT NULL AND p.rank <= p_rank THEN 1 ELSE 0 END + CASE WHEN array_length(p_tags, 1) IS NOT NULL AND p.tags @> p_tags THEN 1 ELSE 0 END + CASE WHEN p_open_now IS TRUE THEN 1 ELSE 0 END + CASE WHEN p_is_24_7 IS TRUE AND p.is_24_7 IS TRUE THEN 1 ELSE 0 END )::float / 5.0 AS metadata_score FROM places p LEFT JOIN restaurants r ON r.place_id = p.id AND ( p_is_restaurant_query IS TRUE OR p_main_category = 'Restaurant' OR p_cuisine IS NOT NULL ) LEFT JOIN restaurant_dishes rd ON rd.restaurant_id = r.id AND p_cuisine IS NOT NULL AND rd.display_title ILIKE '%' || p_cuisine || '%' WHERE -- permanently closed filtre COALESCE(p.permanently_closed, false) = false -- semantic threshold AND (1 - (p.embedding <=> query_embedding)) >= match_threshold -- category filtreleri AND (p_main_category IS NULL OR p.main_category = p_main_category) AND (p_sub_category IS NULL OR p.sub_category = p_sub_category) AND ( p_cuisine IS NULL OR r.cuisine_type = p_cuisine OR rd.display_title ILIKE '%' || p_cuisine || '%' ) -- location filtreleri AND ( p_is_istanbul_route IS TRUE OR p_city IS NULL OR p.city = p_city ) AND ( p_is_istanbul_route IS TRUE OR p_neighborhood IS NULL OR p.neighborhood = p_neighborhood ) -- route stops aggregation AND ( p_use_route_stops IS FALSE OR p.city = ANY(p_route_stops) OR p.neighborhood = ANY(p_route_stops) ) -- destination priority AND ( p_destination_priority IS FALSE OR p.city = p_route_destination OR p.neighborhood = p_route_destination ) -- total_score / rank AND (p_min_rating IS NULL OR p.total_score >= p_min_rating) AND (p_rank IS NULL OR p.rank <= p_rank) -- tags contains AND ( array_length(p_tags, 1) IS NULL OR p.tags @> p_tags ) -- avoid negative filter AND ( array_length(p_avoid, 1) IS NULL OR NOT (p.tags && p_avoid) ) -- 24/7 AND ( p_is_24_7 IS NULL OR p.is_24_7 = p_is_24_7 ) -- proximity null-safe AND ( p_proximity_enabled IS FALSE OR p_user_lat IS NULL OR p_user_lng IS NULL OR p_max_distance_km IS NULL OR ( 6371 * acos( cos(radians(p_user_lat)) * cos(radians(p.latitude)) * cos(radians(p.longitude) - radians(p_user_lng)) + sin(radians(p_user_lat)) * sin(radians(p.latitude)) ) ) <= p_max_distance_km ) ) SELECT base.id, base.display_title, base.city, base.neighborhood, base.main_category, base.sub_category, base.cuisine, base.total_score, base.rank, base.tags, base.latitude, base.longitude, base.is_24_7, base.semantic_similarity, base.metadata_score, ( alpha_weight * base.semantic_similarity + beta_weight * base.metadata_score ) AS candidate_score FROM base ORDER BY candidate_score DESC LIMIT match_count; $$; CREATE OR REPLACE FUNCTION match_pois_metadata_only( match_count int DEFAULT 200, p_city text DEFAULT NULL, p_neighborhood text DEFAULT NULL, p_main_category text DEFAULT NULL, p_sub_category text DEFAULT NULL, p_cuisine text DEFAULT NULL, p_min_rating float DEFAULT NULL, p_rank int DEFAULT NULL, p_tags text[] DEFAULT ARRAY[]::text[], p_avoid text[] DEFAULT ARRAY[]::text[], p_open_now boolean DEFAULT NULL, p_is_24_7 boolean DEFAULT NULL, p_route_enabled boolean DEFAULT false, p_route_origin text DEFAULT NULL, p_route_stops text[] DEFAULT ARRAY[]::text[], p_route_destination text DEFAULT NULL, p_user_lat float DEFAULT NULL, p_user_lng float DEFAULT NULL, p_proximity_enabled boolean DEFAULT false, p_max_distance_km float DEFAULT NULL, p_is_restaurant_query boolean DEFAULT false, p_is_istanbul_route boolean DEFAULT false, p_use_route_stops boolean DEFAULT false, p_destination_priority boolean DEFAULT false ) RETURNS TABLE ( id uuid, display_title text, city text, neighborhood text, main_category text, sub_category text, cuisine text, total_score float, rank int, tags text[], latitude float, longitude float, is_24_7 boolean, metadata_score float, candidate_score float ) LANGUAGE sql AS $$ SELECT p.id, p.display_title, p.city, p.neighborhood, p.main_category, p.sub_category, r.cuisine_type AS cuisine, p.total_score, p.rank, p.tags, p.latitude, p.longitude, p.is_24_7, ( CASE WHEN p_min_rating IS NOT NULL AND p.total_score >= p_min_rating THEN 1 ELSE 0 END + CASE WHEN p_rank IS NOT NULL AND p.rank <= p_rank THEN 1 ELSE 0 END + CASE WHEN array_length(p_tags, 1) IS NOT NULL AND p.tags @> p_tags THEN 1 ELSE 0 END + CASE WHEN p_is_24_7 IS TRUE AND p.is_24_7 IS TRUE THEN 1 ELSE 0 END )::float / 4.0 AS metadata_score, ( CASE WHEN p_min_rating IS NOT NULL AND p.total_score >= p_min_rating THEN 1 ELSE 0 END + CASE WHEN p_rank IS NOT NULL AND p.rank <= p_rank THEN 1 ELSE 0 END + CASE WHEN array_length(p_tags, 1) IS NOT NULL AND p.tags @> p_tags THEN 1 ELSE 0 END + CASE WHEN p_is_24_7 IS TRUE AND p.is_24_7 IS TRUE THEN 1 ELSE 0 END )::float / 4.0 AS candidate_score FROM places p LEFT JOIN restaurants r ON r.place_id = p.id AND ( p_is_restaurant_query IS TRUE OR p_main_category = 'Restaurant' OR p_cuisine IS NOT NULL ) LEFT JOIN restaurant_dishes rd ON rd.restaurant_id = r.id AND p_cuisine IS NOT NULL AND rd.display_title ILIKE '%' || p_cuisine || '%' WHERE COALESCE(p.permanently_closed, false) = false AND (p_main_category IS NULL OR p.main_category = p_main_category) AND (p_sub_category IS NULL OR p.sub_category = p_sub_category) AND ( p_cuisine IS NULL OR r.cuisine_type = p_cuisine OR rd.display_title ILIKE '%' || p_cuisine || '%' ) AND ( p_is_istanbul_route IS TRUE OR p_city IS NULL OR p.city = p_city ) AND ( p_is_istanbul_route IS TRUE OR p_neighborhood IS NULL OR p.neighborhood = p_neighborhood ) AND ( p_use_route_stops IS FALSE OR p.city = ANY(p_route_stops) OR p.neighborhood = ANY(p_route_stops) ) AND ( p_destination_priority IS FALSE OR p.city = p_route_destination OR p.neighborhood = p_route_destination ) AND (p_min_rating IS NULL OR p.total_score >= p_min_rating) AND (p_rank IS NULL OR p.rank <= p_rank) AND ( array_length(p_tags, 1) IS NULL OR p.tags @> p_tags ) AND ( array_length(p_avoid, 1) IS NULL OR NOT (p.tags && p_avoid) ) AND ( p_is_24_7 IS NULL OR p.is_24_7 = p_is_24_7 ) ORDER BY candidate_score DESC LIMIT match_count; $$;