925 lines
24 KiB
Markdown

# Geographic Polygon Overlays (Cuts)
## Overview
The cuts system provides polygon-based geographic organizing using customizable map overlays. Cuts enable campaigns to divide territories into canvassing zones, track completion progress, and assign volunteers to specific areas.
**Key Capabilities:**
- **Polygon Drawing**: Click-to-draw custom polygons on Leaflet maps
- **GeoJSON Storage**: Store complex polygons with coordinate precision
- **Spatial Queries**: Point-in-polygon filtering using ray-casting algorithm
- **Cut Categories**: CUSTOM, WARD, NEIGHBORHOOD, DISTRICT classification
- **Visual Customization**: Configurable colors and opacity for map overlays
- **Bounds Calculation**: Auto-calculate bounding box from polygon coordinates
- **Completion Tracking**: Track canvassing progress by cut
- **Shift Assignment**: Link shifts to cuts for volunteer scheduling
- **Export Filtering**: Generate walk sheets for specific cuts
**Use Cases:**
- Electoral district mapping (wards, polling divisions)
- Canvassing zone organization
- Neighborhood targeting
- Volunteer territory assignment
- Walk sheet generation by area
- Progress tracking by geographic zone
- Multi-volunteer coordination
## Architecture
```mermaid
graph TD
A[Admin User] -->|Draws Polygon| B[CutDrawingMode]
B -->|Click Vertices| C[Leaflet Map]
C -->|Auto-Close Detection| D[GeoJSON Polygon]
D -->|POST /api/map/cuts| E[Cuts Service]
E -->|Calculate Bounds| F[Spatial Utils]
F -->|Save| G[(Cut Model)]
H[Public Map] -->|Load Cuts| I[GET /api/public/map/cuts]
I -->|Return GeoJSON| E
E -->|Query| G
I -->|Render| J[CutOverlays Component]
K[Canvass Session] -->|Start in Cut| L[Canvass Service]
L -->|Load Addresses| M[Locations Service]
M -->|Point-in-Polygon| F
F -->|Filter| N[(Location Model)]
O[Shift] -->|Assigned to Cut| G
G -->|1:N| O
P[Export Locations] -->|Filter by Cut| M
M -->|Query Polygon| F
style G fill:#e1f5ff
style N fill:#e1f5ff
style O fill:#e1f5ff
```
**Flow Description:**
1. **Admin draws cut** → Click vertices on map, auto-close detection, generate GeoJSON
2. **Save cut** → Calculate bounds from coordinates, store polygon in database
3. **Public map loads** → Query public cuts, render as colored overlays with opacity
4. **Canvass session starts** → Load addresses within cut polygon using ray-casting
5. **Shift assignment** → Link shift to cut for volunteer scheduling
6. **Export locations** → Filter by cut polygon to generate walk sheet
## Database Models
### Cut Model
See [Cut Model Documentation](../../database/models/map.md#cut-model) for full schema.
**Key Fields:**
- `name`: Cut display name (e.g., "Ward 5 - Downtown")
- `description`: Free-text notes about the cut
- `geojson`: Polygon coordinates in GeoJSON format (TEXT field)
- `bounds`: Auto-calculated bounding box `{minLat, maxLat, minLng, maxLng}` (JSON)
- `color`: Hex color for map overlay (default: `#3498db`)
- `opacity`: Opacity 0.0-1.0 for map rendering (default: 0.3)
- `category`: CUSTOM | WARD | NEIGHBORHOOD | DISTRICT
- `isPublic`: Show on public map
- `isOfficial`: Official electoral boundary (prevents accidental deletion)
- `showLocations`: Show location markers within cut on map
- `exportEnabled`: Allow walk sheet export for this cut
- `assignedTo`: Free-text assigned volunteer/team name
- `completionPercentage`: Auto-calculated canvassing progress (0-100)
**GeoJSON Format:**
```json
{
"type": "Polygon",
"coordinates": [
[
[-75.6972, 45.4215],
[-75.6980, 45.4220],
[-75.6960, 45.4230],
[-75.6950, 45.4225],
[-75.6972, 45.4215]
]
]
}
```
**Bounds Format:**
```json
{
"minLat": 45.4215,
"maxLat": 45.4230,
"minLng": -75.6980,
"maxLng": -75.6950
}
```
**Related Models:**
- [Shift](../../database/models/map.md#shift-model) — Volunteer shifts assigned to cut
- [CanvassSession](../../database/models/canvass.md#canvasssession-model) — Canvassing within cut
- [Location](../../database/models/map.md#location-model) — Filtered by cut polygon
## API Endpoints
See [Cuts Backend Module Documentation](../../backend/modules/map/cuts.md) for full API reference.
**Admin Endpoints:**
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| GET | `/api/map/cuts` | MAP_ADMIN | List cuts with pagination, search, category filter |
| GET | `/api/map/cuts/stats` | MAP_ADMIN | Get cut statistics (total, by category) |
| GET | `/api/map/cuts/:id` | MAP_ADMIN | Get cut details |
| POST | `/api/map/cuts` | MAP_ADMIN | Create new cut with polygon |
| PATCH | `/api/map/cuts/:id` | MAP_ADMIN | Update cut |
| DELETE | `/api/map/cuts/:id` | MAP_ADMIN | Delete cut (blocked if `isOfficial=true`) |
| GET | `/api/map/cuts/:id/locations` | MAP_ADMIN | Get locations within cut polygon |
| GET | `/api/map/cuts/:id/progress` | MAP_ADMIN | Get canvassing progress for cut |
**Public Endpoints:**
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| GET | `/api/public/map/cuts` | None | List public cuts (isPublic=true) |
| GET | `/api/public/map/cuts/:id` | None | Get public cut details |
## Configuration
### Environment Variables
No specific environment variables for cuts. Uses standard database and map settings.
### Cut Category Enum
```typescript
enum CutCategory {
CUSTOM // User-defined boundary
WARD // Municipal ward boundary
NEIGHBORHOOD // Neighborhood association boundary
DISTRICT // Electoral district boundary
}
```
### Default Values
| Field | Default | Description |
|-------|---------|-------------|
| `color` | `#3498db` | Blue color for overlay |
| `opacity` | `0.3` | 30% opacity (transparent) |
| `isPublic` | `false` | Hidden from public map |
| `isOfficial` | `false` | Can be deleted by admin |
| `showLocations` | `true` | Show location markers within cut |
| `exportEnabled` | `true` | Allow walk sheet export |
| `completionPercentage` | `0` | Auto-updated by canvass service |
## Admin Workflow
### Creating a Cut
**Step 1: Navigate to Cuts Page**
Navigate to **Map → Cuts** in the admin sidebar.
![CutsPage Screenshot Placeholder]
**Step 2: Open Drawing Tab**
Click **Drawing** tab to switch to map drawing mode.
**Step 3: Activate Drawing Mode**
Click **Draw Cut** button in the map controls. Map cursor changes to crosshair.
**Step 4: Click Vertices**
Click on the map to place polygon vertices:
- **First Click**: Start polygon
- **Additional Clicks**: Add vertices
- **Auto-Close**: When cursor near start point (within 10px), polygon auto-closes
**Step 5: Configure Cut**
Fill in the cut form (right sidebar):
- **Name**: "Ward 5 - Downtown"
- **Description**: "Central business district and residential blocks"
- **Category**: WARD
- **Color**: Choose from color picker (default: blue)
- **Opacity**: Slider 0-100 (default: 30%)
- **Is Public**: Toggle to show on public map
- **Is Official**: Toggle to prevent accidental deletion
**Step 6: Save Cut**
Click **Save Cut**. The system will:
1. Generate GeoJSON from vertices
2. Calculate bounding box
3. Save to database
4. Render polygon on map with configured color/opacity
### Editing a Cut
**Step 1: Select Cut**
On **Table** tab, click **Edit** button for a cut.
**Step 2: Update Fields**
Modify cut properties:
- **Name/Description**: Update text fields
- **Color/Opacity**: Adjust visual appearance
- **Category**: Change classification
- **Public/Official**: Toggle flags
**Step 3: Re-Draw Polygon (Optional)**
To change polygon shape:
1. Switch to **Drawing** tab
2. Click **Edit Cut** button
3. Delete old vertices (click vertices to remove)
4. Add new vertices
5. Auto-close polygon
**Step 4: Save Changes**
Click **Update** to save changes. Bounds are auto-recalculated if polygon changed.
### Viewing Locations in Cut
**Step 1: Select Cut**
Click cut row in table to select.
**Step 2: Click "View Locations"**
Click **View Locations** button.
**Step 3: View Filtered Table**
System displays locations within cut polygon:
- **Point-in-Polygon**: Uses ray-casting algorithm to filter
- **Count**: Number of locations within cut
- **Support Breakdown**: Count by support level
**Step 4: Export Locations**
Click **Export CSV** to download locations for walk sheet generation.
### Assigning Cut to Shift
**Step 1: Create/Edit Shift**
On **Map → Shifts** page, create or edit a shift.
**Step 2: Select Cut**
In shift form, choose cut from **Cut** dropdown.
**Step 3: Save Shift**
Shift is now linked to cut. Volunteers will see cut name on shift details.
### Tracking Cut Completion
**Step 1: View Cut Progress**
On CutsPage, click **Progress** button for a cut.
**Step 2: View Metrics**
System displays:
- **Completion Percentage**: Auto-calculated from canvass visits
- **Total Addresses**: Count of addresses within cut
- **Visited**: Count of addresses with CanvassVisit records
- **Outstanding**: Remaining addresses to visit
**Step 3: View Canvass Activity**
Table shows recent canvass visits within cut:
- **Volunteer Name**: Who visited
- **Visit Date**: When visited
- **Outcome**: Visit result (SPOKE_WITH, NOT_HOME, etc.)
- **Support Level**: Updated support level (if applicable)
## Public Workflow
**Public users can view cut overlays on the interactive map.**
**Step 1: Navigate to Public Map**
Visit `/map` (no authentication required).
**Step 2: Toggle Cut Overlays**
Click **Cuts** button in map controls to open overlay panel.
**Step 3: Select Cuts**
Check/uncheck cuts to show/hide on map:
- **Color Legend**: Shows cut name and color
- **Opacity**: Semi-transparent overlays don't obscure markers
- **Multiple Cuts**: Show multiple cuts simultaneously
**Step 4: View Cut Details**
Click on a cut polygon to view:
- **Cut Name**: Displayed in popup
- **Category**: Ward, Neighborhood, etc.
- **Assigned To**: Volunteer/team name (if configured)
## Volunteer Workflow
**Volunteers interact with cuts via shift assignments.**
**Step 1: View Assigned Shifts**
On **Volunteer → My Assignments** page, view shifts with cut assignments.
**Step 2: Start Canvass Session**
Click **Start Canvass** on a shift. Redirects to `/volunteer/canvass/:cutId`.
**Step 3: View Cut on Map**
Full-screen map shows:
- **Cut Polygon**: Highlighted boundary
- **Locations Within Cut**: Filtered to cut polygon only
- **Walking Route**: Optimal route through cut locations
See [Canvassing Documentation](./canvassing.md) for full volunteer workflow.
## Code Examples
### Cut Service Create (Backend)
```typescript
// api/src/modules/map/cuts/cuts.service.ts
import { parseGeoJsonPolygon, calculateBounds } from '../../../utils/spatial';
async create(data: CreateCutInput, userId: string) {
// Auto-calculate bounds from geojson if not provided
let boundsStr = data.bounds;
if (!boundsStr) {
try {
const rings = parseGeoJsonPolygon(data.geojson);
const allCoords = rings.flat();
const bounds = calculateBounds(allCoords);
boundsStr = JSON.stringify(bounds);
} catch {
// Bounds calculation optional
}
}
const cut = await prisma.cut.create({
data: {
name: data.name,
description: data.description,
color: data.color,
opacity: data.opacity,
category: data.category,
isPublic: data.isPublic,
isOfficial: data.isOfficial,
geojson: data.geojson,
bounds: boundsStr,
showLocations: data.showLocations,
exportEnabled: data.exportEnabled,
assignedTo: data.assignedTo,
createdByUserId: userId,
},
});
return cut;
}
```
### Bounds Calculation (Backend)
```typescript
// api/src/utils/spatial.ts
export function calculateBounds(coordinates: number[][]): {
minLat: number;
maxLat: number;
minLng: number;
maxLng: number;
} {
let minLat = Infinity;
let maxLat = -Infinity;
let minLng = Infinity;
let maxLng = -Infinity;
for (const coord of coordinates) {
const lng = coord[0]!;
const lat = coord[1]!;
if (lat < minLat) minLat = lat;
if (lat > maxLat) maxLat = lat;
if (lng < minLng) minLng = lng;
if (lng > maxLng) maxLng = lng;
}
return { minLat, maxLat, minLng, maxLng };
}
```
### Point-in-Polygon Filter (Backend)
```typescript
// api/src/modules/map/cuts/cuts.service.ts
import { isPointInPolygon, parseGeoJsonPolygon } from '../../../utils/spatial';
async getLocationsInCut(cutId: string) {
const cut = await prisma.cut.findUnique({
where: { id: cutId },
select: { geojson: true },
});
if (!cut?.geojson) {
throw new AppError(404, 'Cut not found', 'CUT_NOT_FOUND');
}
// Get all locations (or use bounds for optimization)
const locations = await prisma.location.findMany({
select: {
id: true,
latitude: true,
longitude: true,
address: true,
},
});
// Parse polygon coordinates
const polygons = parseGeoJsonPolygon(cut.geojson);
// Filter locations using ray-casting algorithm
const filtered = locations.filter((loc) => {
const lat = Number(loc.latitude);
const lng = Number(loc.longitude);
return polygons.some((poly) => isPointInPolygon(lat, lng, poly));
});
return filtered;
}
```
### Ray-Casting Algorithm (Backend)
```typescript
// api/src/utils/spatial.ts
export function isPointInPolygon(
lat: number,
lng: number,
polygonCoords: number[][]
): boolean {
let inside = false;
for (let i = 0, j = polygonCoords.length - 1; i < polygonCoords.length; j = i++) {
const xi = polygonCoords[i]![1]!; // lat
const yi = polygonCoords[i]![0]!; // lng
const xj = polygonCoords[j]![1]!;
const yj = polygonCoords[j]![0]!;
const intersect = ((yi > lng) !== (yj > lng)) &&
(lat < (xj - xi) * (lng - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
}
```
### Cut Drawing Mode (Frontend)
```typescript
// admin/src/components/map/CutDrawingMode.tsx
import { useState, useEffect } from 'react';
import { useMapEvents } from 'react-leaflet';
import type { LatLng } from 'leaflet';
interface CutDrawingModeProps {
onPolygonComplete: (vertices: LatLng[]) => void;
}
export default function CutDrawingMode({ onPolygonComplete }: CutDrawingModeProps) {
const [vertices, setVertices] = useState<LatLng[]>([]);
const [isDrawing, setIsDrawing] = useState(true);
useMapEvents({
click(e) {
if (!isDrawing) return;
const newVertex = e.latlng;
// Auto-close detection: if click near first vertex (within 10px)
if (vertices.length >= 3) {
const firstVertex = vertices[0]!;
const map = e.target;
const firstPoint = map.latLngToContainerPoint(firstVertex);
const newPoint = map.latLngToContainerPoint(newVertex);
const distance = Math.sqrt(
Math.pow(firstPoint.x - newPoint.x, 2) +
Math.pow(firstPoint.y - newPoint.y, 2)
);
if (distance < 10) {
// Auto-close polygon
setIsDrawing(false);
onPolygonComplete(vertices);
return;
}
}
// Add vertex
setVertices([...vertices, newVertex]);
},
});
return (
<>
{/* Render temporary polygon while drawing */}
{vertices.length >= 2 && (
<Polygon positions={vertices} pathOptions={{ color: '#3498db', opacity: 0.5 }} />
)}
{/* Render vertex markers */}
{vertices.map((v, i) => (
<CircleMarker
key={i}
center={v}
radius={5}
pathOptions={{ color: '#e74c3c', fillColor: '#e74c3c', fillOpacity: 1 }}
/>
))}
</>
);
}
```
### Cut Overlays Rendering (Frontend)
```typescript
// admin/src/components/map/CutOverlays.tsx
import { Polygon, Popup } from 'react-leaflet';
import type { Cut } from '@/types/api';
interface CutOverlaysProps {
cuts: Cut[];
visibleCutIds: string[];
}
export default function CutOverlays({ cuts, visibleCutIds }: CutOverlaysProps) {
return (
<>
{cuts
.filter((cut) => visibleCutIds.includes(cut.id))
.map((cut) => {
const geojson = JSON.parse(cut.geojson);
// GeoJSON uses [lng, lat], Leaflet uses [lat, lng]
const positions = geojson.coordinates[0].map(([lng, lat]: number[]) => [lat, lng]);
return (
<Polygon
key={cut.id}
positions={positions}
pathOptions={{
color: cut.color,
fillColor: cut.color,
fillOpacity: cut.opacity,
weight: 2,
}}
>
<Popup>
<div>
<strong>{cut.name}</strong>
<br />
{cut.category}
{cut.assignedTo && (
<>
<br />
Assigned to: {cut.assignedTo}
</>
)}
</div>
</Popup>
</Polygon>
);
})}
</>
);
}
```
### Convert Leaflet Polygon to GeoJSON (Frontend)
```typescript
// admin/src/pages/CutsPage.tsx
const handleSaveCut = async (vertices: LatLng[]) => {
// Convert Leaflet [lat, lng] to GeoJSON [lng, lat]
const coordinates = vertices.map((v) => [v.lng, v.lat]);
// Close polygon (first vertex === last vertex)
coordinates.push(coordinates[0]!);
const geojson = {
type: 'Polygon',
coordinates: [coordinates],
};
try {
const { data } = await api.post<Cut>('/map/cuts', {
name: cutName,
description: cutDescription,
geojson: JSON.stringify(geojson),
color: cutColor,
opacity: cutOpacity,
category: cutCategory,
isPublic: isPublic,
isOfficial: isOfficial,
});
message.success('Cut created');
fetchCuts();
} catch (error) {
message.error('Failed to create cut');
}
};
```
## Troubleshooting
### Issue: Polygon Not Closing
**Symptoms:**
- Clicking near start point doesn't auto-close polygon
- Polygon remains open after many vertices
- "Save Cut" button disabled
**Causes:**
- Auto-close distance threshold too small
- Mouse click precision issues on mobile
- Map zoom level affecting pixel distance calculation
**Solutions:**
1. **Increase auto-close threshold**:
```typescript
// admin/src/components/map/CutDrawingMode.tsx
const AUTO_CLOSE_DISTANCE_PX = 15; // Was 10, increase to 15
if (distance < AUTO_CLOSE_DISTANCE_PX) {
// Auto-close polygon
}
```
2. **Manual close button**:
Add explicit "Close Polygon" button for mobile users:
```typescript
<Button onClick={() => {
if (vertices.length >= 3) {
onPolygonComplete(vertices);
}
}}>
Close Polygon
</Button>
```
### Issue: Point-in-Polygon Returns Wrong Results
**Symptoms:**
- Locations outside cut polygon included in canvass session
- Locations inside cut polygon excluded
- Export CSV missing locations
**Causes:**
- Coordinate order mismatch (GeoJSON [lng, lat] vs Leaflet [lat, lng])
- Polygon not properly closed (first vertex !== last vertex)
- Ray-casting algorithm bug with edge cases
**Solutions:**
1. **Verify coordinate order**:
```typescript
// GeoJSON uses [lng, lat]
const geojson = {
type: 'Polygon',
coordinates: [
[
[-75.6972, 45.4215], // [lng, lat]
[-75.6980, 45.4220],
// ...
]
]
};
// Leaflet uses [lat, lng]
<Polygon positions={[[45.4215, -75.6972], [45.4220, -75.6980]]} />
```
2. **Verify polygon closure**:
```sql
-- Check if polygon is properly closed
SELECT id, name,
geojson::json->'coordinates'->0->0 as first_vertex,
geojson::json->'coordinates'->0->-1 as last_vertex
FROM "Cut"
WHERE id = 'YOUR_CUT_ID';
-- First and last should be identical
```
3. **Test with known points**:
```bash
# Test point-in-polygon directly
curl -X POST http://localhost:4000/api/map/cuts/YOUR_CUT_ID/test-point \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"latitude":45.4220,"longitude":-75.6975}'
```
### Issue: Cut Rendering Performance Slow
**Symptoms:**
- Map lags when rendering multiple cuts
- Browser freezes with >10 cuts visible
- Polygon rendering takes >2 seconds
**Causes:**
- Too many polygon vertices (complex boundaries)
- Multiple cut overlays rendered simultaneously
- No polygon simplification
**Solutions:**
1. **Simplify complex polygons**:
Use Turf.js simplify algorithm to reduce vertices:
```typescript
import * as turf from '@turf/turf';
const simplified = turf.simplify(polygon, {
tolerance: 0.0001, // Adjust based on zoom level
highQuality: true
});
```
2. **Lazy render cuts**:
Only render cuts within current map bounds:
```typescript
const visibleCuts = cuts.filter((cut) => {
const bounds = JSON.parse(cut.bounds);
const mapBounds = map.getBounds();
return mapBounds.intersects([
[bounds.minLat, bounds.minLng],
[bounds.maxLat, bounds.maxLng]
]);
});
```
3. **Use Canvas renderer**:
For large polygons, use Leaflet Canvas renderer instead of SVG:
```typescript
<Polygon
positions={positions}
renderer={L.canvas()}
pathOptions={{ color: cut.color }}
/>
```
## Performance Considerations
### Spatial Query Optimization
**Bounds Pre-Filter:**
Always pre-filter by bounding box before point-in-polygon:
```typescript
async getLocationsInCut(cutId: string) {
const cut = await prisma.cut.findUnique({ where: { id: cutId } });
const bounds = JSON.parse(cut.bounds);
// Pre-filter by bounds (fast, uses index)
const candidates = await prisma.location.findMany({
where: {
latitude: {
gte: new Prisma.Decimal(bounds.minLat),
lte: new Prisma.Decimal(bounds.maxLat),
},
longitude: {
gte: new Prisma.Decimal(bounds.minLng),
lte: new Prisma.Decimal(bounds.maxLng),
},
},
});
// Then apply point-in-polygon (slower, but fewer candidates)
const polygons = parseGeoJsonPolygon(cut.geojson);
return candidates.filter((loc) => {
const lat = Number(loc.latitude);
const lng = Number(loc.longitude);
return polygons.some((poly) => isPointInPolygon(lat, lng, poly));
});
}
```
**Performance Impact:**
- **Without bounds pre-filter**: 10,000 locations → 10,000 point-in-polygon checks
- **With bounds pre-filter**: 10,000 locations → 500 candidates → 500 point-in-polygon checks (20x faster)
### Polygon Simplification
**Reduce Vertices for Large Cuts:**
Use Douglas-Peucker algorithm to simplify polygons while preserving shape:
```typescript
import * as turf from '@turf/turf';
function simplifyPolygon(geojson: string, tolerance: number = 0.0001): string {
const polygon = JSON.parse(geojson);
const simplified = turf.simplify(polygon, { tolerance, highQuality: true });
return JSON.stringify(simplified);
}
// Usage: simplify when importing official boundaries (e.g., electoral districts)
const simplifiedGeojson = simplifyPolygon(officialBoundary, 0.0005);
```
**Tolerance Guidelines:**
- **0.00001**: High precision (±1m), use for small neighborhoods
- **0.0001**: Medium precision (±10m), use for wards
- **0.001**: Low precision (±100m), use for large districts
### Caching Cut Queries
**Cache Frequently Used Cuts:**
```typescript
// Cache cut polygons in Redis for fast repeated queries
const CACHE_KEY = `CUT_POLYGON:${cutId}`;
const cached = await redis.get(CACHE_KEY);
if (cached) {
return JSON.parse(cached);
}
const cut = await prisma.cut.findUnique({ where: { id: cutId } });
await redis.setex(CACHE_KEY, 3600, JSON.stringify(cut)); // 1 hour TTL
return cut;
```
## Related Documentation
**Backend Modules:**
- [Cuts Backend Module](../../backend/modules/map/cuts.md) — API implementation
- [Spatial Utils](../../backend/modules/utils/spatial.md) — Point-in-polygon algorithms
- [Locations Service](../../backend/modules/map/locations.md) — Spatial filtering
**Frontend Pages:**
- [CutsPage](../../frontend/pages/admin/cuts-page.md) — Admin CRUD interface
- [CutDrawingMode](../../frontend/components/cut-drawing-mode.md) — Polygon drawing
- [CutOverlays](../../frontend/components/cut-overlays.md) — Map rendering
**Database:**
- [Cut Model](../../database/models/map.md#cut-model) — Cut schema
- [Spatial Queries](../../database/queries.md#spatial-queries) — Optimization tips
**Features:**
- [Locations](./locations.md) — Location filtering by cut
- [Shifts](./shifts.md) — Shift assignment to cuts
- [Canvassing](./canvassing.md) — Canvassing within cut boundaries
- [Walk Sheets](./walk-sheets.md) — Export locations by cut