# Map Models ## Overview The Map module provides building-level and unit-level location management with multi-provider geocoding, volunteer shift scheduling, GeoJSON polygon cuts for map filtering, and comprehensive audit trails. **Models (7):** - Location — Building-level with lat/lng, NAR integration - Address — Unit-level with support levels - LocationHistory — Audit trail (7 action types) - Shift — Volunteer shifts with cut relation - ShiftSignup — Signup tracking - Cut — GeoJSON polygon overlays - MapSettings — Singleton configuration **Key Features:** - Building vs unit architecture (1 Location → many Addresses) - Multi-provider geocoding (6 providers: Google, Mapbox, Nominatim, Photon, LocationIQ, ArcGIS) - NAR 2025 Canadian electoral data import - Spatial indexing (latitude/longitude composite index) - GeoJSON polygon storage for cuts - Walk sheet generation with QR codes - CSV import/export See [Schema Reference](../schema.md#map--locations) for complete field listings. --- ## Building vs Unit Architecture **Location** = Building-level data: - Single lat/lng coordinate - Street address (no unit number) - Building type (SINGLE_FAMILY, MULTI_UNIT, MIXED_USE, COMMERCIAL) - Total units count - Building notes (access codes, manager contact) **Address** = Unit-level data: - Unit number (apartment #, suite #) - Occupant name/email/phone - Support level (1-4) - Sign request flag - Canvassing notes **Relationship:** `Location ||--o{ Address` (one-to-many) **Example:** ```typescript // 1 Location: 123 Main St (4-unit apartment building) const location = { address: '123 Main St', latitude: 53.5461, longitude: -113.4938, buildingType: 'MULTI_UNIT', totalUnits: 4, }; // 4 Addresses: Units 101-104 const addresses = [ { locationId, unitNumber: '101', firstName: 'Alice', supportLevel: '4' }, { locationId, unitNumber: '102', firstName: 'Bob', supportLevel: '3' }, { locationId, unitNumber: '103', firstName: 'Carol', supportLevel: '2' }, { locationId, unitNumber: '104', firstName: 'Dave', supportLevel: '1' }, ]; ``` --- ## Geocoding Providers ```prisma enum GeocodeProvider { GOOGLE // Google Maps Geocoding API MAPBOX // Mapbox Geocoding API NOMINATIM // OpenStreetMap Nominatim PHOTON // Photon (OSM-based) LOCATIONIQ // LocationIQ (OSM-based) ARCGIS // ArcGIS Geocoding Service UNKNOWN // Manually entered or unknown source } ``` **Provider Priority:** 1. Google (highest accuracy, paid) 2. Mapbox (high accuracy, paid) 3. ArcGIS (high accuracy, free tier) 4. Nominatim (medium accuracy, free) 5. Photon (medium accuracy, free) 6. LocationIQ (medium accuracy, free tier) **Confidence Score:** 0-100 (stored in `geocodeConfidence` field) --- ## NAR 2025 Import **NAR** = National Address Register (Canadian electoral data) **Import Features:** - Streams large CSV files (no memory limit) - Joins Location + Address files on LOC_GUID - Converts BG_X/BG_Y (EPSG:3347 Lambert projection) → lat/lng - Province selector (codes 10-62) - City/postal/cut filters - Residential-only toggle (buildingUse = 1) **New Location Fields:** - `postalCode` — Canadian postal code - `province` — Province code (e.g., "AB") - `federalDistrict` — Federal electoral district - `buildingUse` — NAR BU_USE (1=Residential, 2=Partial, 3=Non-Residential, 4=Unknown) - `locGuid` — NAR LOC_GUID (unique) **New Address Fields:** - `addrGuid` — NAR ADDR_GUID (unique) --- ## LocationHistory Actions ```prisma enum LocationHistoryAction { CREATED // Location created UPDATED // Location updated GEOCODED // Single location geocoded BULK_GEOCODED // Batch geocode operation MOVED_ON_MAP // Dragged on admin map IMPORTED_CSV // CSV import IMPORTED_NAR // NAR import } ``` **Audit Fields:** - `field` — Which field changed (e.g., "latitude") - `oldValue` — Previous value - `newValue` — New value - `metadata` — JSON with provider, confidence, etc. --- ## Cut GeoJSON Storage **Cut** stores GeoJSON polygon coordinates: ```json { "type": "Polygon", "coordinates": [ [ [-113.5, 53.5], [-113.4, 53.5], [-113.4, 53.6], [-113.5, 53.6], [-113.5, 53.5] ] ] } ``` **Bounds:** Calculated bounding box for quick filtering: ```json { "north": 53.6, "south": 53.5, "east": -113.4, "west": -113.5 } ``` --- ## Shift Status Workflow ```mermaid stateDiagram-v2 [*] --> OPEN : Create shift OPEN --> FULL : currentVolunteers >= maxVolunteers OPEN --> CANCELLED : Admin cancels FULL --> OPEN : Volunteer cancels (currentVolunteers < maxVolunteers) FULL --> CANCELLED : Admin cancels CANCELLED --> [*] ``` --- ## Common Queries ### Create Location with Geocoding ```typescript const location = await prisma.location.create({ data: { address: '123 Main St, Edmonton, AB', latitude: 53.5461, longitude: -113.4938, geocodeProvider: GeocodeProvider.GOOGLE, geocodeConfidence: 95, buildingType: BuildingType.SINGLE_FAMILY, totalUnits: 1, createdByUserId: user.id, history: { create: { userId: user.id, action: LocationHistoryAction.GEOCODED, metadata: { provider: 'google', confidence: 95 }, }, }, }, }); ``` ### Find Locations in Bounding Box ```typescript const locations = await prisma.location.findMany({ where: { latitude: { gte: 53.5, lte: 53.6 }, longitude: { gte: -113.5, lte: -113.4 }, }, include: { addresses: true }, }); ``` ### Create Shift with Cut ```typescript const shift = await prisma.shift.create({ data: { title: 'Weekend Canvassing - Downtown', date: new Date('2025-02-15'), startTime: '10:00', endTime: '14:00', maxVolunteers: 10, isPublic: true, cutId: cut.id, // Assign to cut }, }); ``` ### Public Shift Signup (Creates TEMP User) ```typescript // 1. Create TEMP user with random password const tempPassword = generatePassword(); // "SwiftEagle42" const tempUser = await prisma.user.create({ data: { email: 'volunteer@example.com', password: await bcrypt.hash(tempPassword, 10), name: 'Jane Volunteer', role: UserRole.TEMP, createdVia: UserCreatedVia.PUBLIC_SHIFT_SIGNUP, expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days expireDays: 30, }, }); // 2. Create shift signup await prisma.shiftSignup.create({ data: { shiftId: shift.id, userId: tempUser.id, userEmail: 'volunteer@example.com', userName: 'Jane Volunteer', signupSource: SignupSource.PUBLIC, }, }); // 3. Send confirmation email with temp password await emailService.send({ template: 'shift-signup-confirmation', variables: { USER_NAME: 'Jane Volunteer', SHIFT_TITLE: shift.title, IS_NEW_USER: 'true', TEMP_PASSWORD: tempPassword, // ... }, }); ``` --- ## Related Documentation - [Schema Reference](../schema.md#map--locations) — Complete field listings - [Database Overview](../index.md) — ER diagram - [API Map Routes](../../api/map.md) — REST endpoints - [Admin Locations Page](../../admin/locations.md) — Location management UI - [Admin Cuts Page](../../admin/cuts.md) — Cut editor UI - [Public Map Page](../../admin/public-map.md) — Public map UI