/** * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * Project: Groupe Ka / Ka Maps * * Canonical types shared by every Groupe Ka application. * Each app maps its own domain model into these shapes at the adapter * boundary — no app-specific field ever lives here. */ /** Which Groupe Ka application produced a map item. */ export type KaAppSource = "lou-ka" | "immo-ka" | "vrai-prix"; /** Canonical geographic representation of a property-like item. */ export interface MapProperty { id: string; appSource: KaAppSource; latitude: number; longitude: number; kind: "listing" | "valuation" | "transaction"; listingType?: "sale" | "rent"; price?: number; estimatedValue?: number; /** 0–1 (or letter grade mapped to 0–1 by the adapter). */ valuationConfidence?: number; propertyType?: string; bedrooms?: number; bathrooms?: number; address?: string; city?: string; region?: string; daysOnMarket?: number; /** Relative price change since listing, e.g. -0.05 for a 5 % cut. */ priceChange?: number; thumbnailUrl?: string; originalUrl?: string; /** App-defined emphasis (e.g. Immo-Ka "sous l'estimation Vrai-Prix"). */ highlight?: boolean; /** Opaque app-owned payload carried through to previews/cards * (availability, source, confidence…). Never read by the framework. */ extra?: Record; } /** Geographic bounding box, always [west, south, east, north]. */ export interface BBox { west: number; south: number; east: number; north: number; } /** Serializable map camera + interaction state. */ export interface KaMapState { center: { lat: number; lng: number }; zoom: number; bearing: number; pitch: number; bounds: BBox | null; selectedPropertyId: string | null; hoveredPropertyId: string | null; activeLayers: string[]; /** True when the viewport moved away from the last searched area. */ searchAreaDirty: boolean; /** GeoJSON geometry drawn with Ka Lens, if any. */ drawnGeometry: GeoJSON.Polygon | null; } /** Aggregated market statistics for an administrative geography. */ export interface GeographicMarketSummary { geographyId: string; geographyType: | "province" | "region" | "municipality" | "borough" | "neighbourhood"; name: string; listingCount: number; medianListingPrice?: number; medianSalePrice?: number; medianEstimatedValue?: number; medianRent?: number; medianDaysOnMarket?: number; change7d?: number; change30d?: number; change1y?: number; } /** Metrics accepted by the generic heatmap infrastructure. */ export type HeatmapMetric = | "listing_density" | "median_price" | "estimated_value" | "price_per_sqft" | "yoy_change" | "days_on_market"; /** Statistics computed by Ka Lens over a drawn or visible area. */ export interface KaLensStats { count: number; medianPrice?: number; medianEstimatedValue?: number; medianPricePerSqft?: number; change30d?: number; change1y?: number; medianDaysOnMarket?: number; priceCutShare?: number; stale90dShare?: number; /** Property-type mix, e.g. { "Maison": 0.52, "Condo": 0.41 }. */ typeMix?: Record; } /** Query sent to an app's data adapter when the viewport settles. */ export interface BoundsQuery { bbox: BBox; zoom: number; /** Opaque app-owned filter payload, forwarded verbatim to the adapter. */ filters?: Record; signal: AbortSignal; } /** Result returned by an app's data adapter. */ export interface BoundsQueryResult { properties: MapProperty[]; /** Total matching server-side (may exceed properties.length). */ totalCount?: number; } /** * The single integration seam between an app and Ka Maps: how that app's * listings/valuations become MapProperty objects for a given viewport. */ export interface KaDataAdapter { /** Stable id, used for cache keys and telemetry. */ id: string; appSource: KaAppSource; fetchInBounds(query: BoundsQuery): Promise; } /** Declaration of one toggleable map layer in the per-app registry. */ export interface KaLayerDefinition { id: string; /** Display group, e.g. "property" | "market" | "land" | "lifestyle" | "investment". */ group: string; label: string; /** Only layers with available data should be marked supported. */ supported: boolean; defaultActive?: boolean; }