package repository import ( "context" "errors" "fmt" "slices" "strconv" "strings" "time" "fire-safety-ymd/internal/domain" "github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgxpool" ) const mcpSpatialSRID = 4326 // PostGISOptions configures the read-only PostgreSQL/PostGIS connection pool. type PostGISOptions struct { DSN string MaxConns int32 ConnectTimeout time.Duration QueryTimeout time.Duration } // TableAudit is a safe schema and geometry summary that contains no business rows. type TableAudit struct { Table string `json:"table"` RowCount int64 `json:"row_count"` NullGeometryCount int64 `json:"null_geometry_count"` EmptyGeometryCount int64 `json:"empty_geometry_count"` InvalidCount int64 `json:"invalid_geometry_count"` OutOfBoundsCount int64 `json:"wgs84_out_of_bounds_count"` GeometryTypes []string `json:"geometry_types"` SRIDs []int `json:"srids"` HasGISTIndex bool `json:"has_gist_geometry_index"` } // ReadinessReport contains non-sensitive PostGIS readiness information. type ReadinessReport struct { PostGISVersion string `json:"postgis_version"` Tables []TableAudit `json:"tables"` Warnings []string `json:"warnings"` } // PostGIS implements the fixed read-only spatial repository over pgxpool. type PostGIS struct { pool *pgxpool.Pool } type safeConnectionError struct { message string cause error } func (e *safeConnectionError) Error() string { return e.message } func (e *safeConnectionError) Unwrap() error { return e.cause } // OpenPostGIS opens and verifies a read-only PostgreSQL connection pool. func OpenPostGIS(ctx context.Context, options PostGISOptions) (*PostGIS, error) { if strings.TrimSpace(options.DSN) == "" || options.MaxConns <= 0 || options.ConnectTimeout <= 0 || options.QueryTimeout <= 0 { return nil, errors.New("invalid PostGIS options") } poolConfig, err := pgxpool.ParseConfig(options.DSN) if err != nil { return nil, &safeConnectionError{message: "parse PostGIS configuration", cause: err} } poolConfig.MaxConns = options.MaxConns poolConfig.ConnConfig.ConnectTimeout = options.ConnectTimeout poolConfig.ConnConfig.RuntimeParams["application_name"] = "fire-safety-ymd-mcp" poolConfig.ConnConfig.RuntimeParams["default_transaction_read_only"] = "on" queryTimeoutMilliseconds := options.QueryTimeout.Milliseconds() if queryTimeoutMilliseconds < 1 { queryTimeoutMilliseconds = 1 } poolConfig.ConnConfig.RuntimeParams["statement_timeout"] = strconv.FormatInt(queryTimeoutMilliseconds, 10) poolConfig.ConnConfig.RuntimeParams["lock_timeout"] = strconv.FormatInt(queryTimeoutMilliseconds, 10) pool, err := pgxpool.NewWithConfig(ctx, poolConfig) if err != nil { return nil, &safeConnectionError{message: "create PostGIS connection pool", cause: err} } connectCtx, cancel := context.WithTimeout(ctx, options.ConnectTimeout) defer cancel() if err := pool.Ping(connectCtx); err != nil { pool.Close() return nil, &safeConnectionError{message: "connect to PostGIS", cause: err} } return &PostGIS{pool: pool}, nil } // Close releases all PostgreSQL connections. func (p *PostGIS) Close() { if p != nil && p.pool != nil { p.pool.Close() } } // Audit inspects PostGIS and the allowlisted source tables without returning record values. func (p *PostGIS) Audit(ctx context.Context) (ReadinessReport, error) { var report ReadinessReport if err := p.pool.QueryRow(ctx, `SELECT postgis_version()`).Scan(&report.PostGISVersion); err != nil { return ReadinessReport{}, fmt.Errorf("query PostGIS version: %w", err) } for _, spec := range spatialTableSpecs { audit, err := p.auditTable(ctx, spec.name) if err != nil { return ReadinessReport{}, fmt.Errorf("audit allowlisted table %s: %w", spec.name, err) } report.Tables = append(report.Tables, audit) report.Warnings = append(report.Warnings, tableAuditWarnings(audit)...) } if report.Warnings == nil { report.Warnings = []string{} } return report, nil } // ValidateForMCP rejects unsafe spatial metadata while allowing queries to exclude invalid records. func (p *PostGIS) ValidateForMCP(ctx context.Context, expectedSRID int) (ReadinessReport, error) { report, err := p.Audit(ctx) if err != nil { return ReadinessReport{}, err } if err := validateReadiness(report, expectedSRID); err != nil { return report, err } return report, nil } func validateReadiness(report ReadinessReport, expectedSRID int) error { if expectedSRID != mcpSpatialSRID { return fmt.Errorf("unsupported MCP spatial SRID: %d", expectedSRID) } if len(report.Tables) != len(spatialTableSpecs) { return fmt.Errorf("readiness report contains %d tables, want %d", len(report.Tables), len(spatialTableSpecs)) } for index, audit := range report.Tables { spec := spatialTableSpecs[index] if audit.Table != spec.name { return fmt.Errorf("readiness report table %d is %s, want %s", index, audit.Table, spec.name) } if audit.OutOfBoundsCount > 0 { return fmt.Errorf("table %s contains coordinates outside WGS84 bounds", audit.Table) } for _, srid := range audit.SRIDs { if srid != expectedSRID { return fmt.Errorf("table %s contains unexpected SRID %d", audit.Table, srid) } } for _, geometryType := range audit.GeometryTypes { if !slices.Contains(spec.geometryTypes, geometryType) { return fmt.Errorf("table %s contains unexpected geometry type %s", audit.Table, geometryType) } } } return nil } func tableAuditWarnings(audit TableAudit) []string { warnings := make([]string, 0, 5) if audit.RowCount == 0 { warnings = append(warnings, audit.Table+":empty_table") } if audit.NullGeometryCount > 0 { warnings = append(warnings, audit.Table+":null_geometries_excluded") } if audit.EmptyGeometryCount > 0 { warnings = append(warnings, audit.Table+":empty_geometries_excluded") } if audit.InvalidCount > 0 { warnings = append(warnings, audit.Table+":invalid_geometries_excluded") } if !audit.HasGISTIndex { warnings = append(warnings, audit.Table+":missing_gist_geometry_index") } return warnings } func (p *PostGIS) auditTable(ctx context.Context, table string) (TableAudit, error) { query := fmt.Sprintf(` SELECT count(*)::bigint, count(*) FILTER (WHERE geom IS NULL)::bigint, count(*) FILTER (WHERE geom IS NOT NULL AND ST_IsEmpty(geom))::bigint, count(*) FILTER (WHERE geom IS NOT NULL AND NOT ST_IsValid(geom))::bigint, count(*) FILTER ( WHERE geom IS NOT NULL AND NOT ST_IsEmpty(geom) AND ( ST_XMin(Box3D(geom)) < -180 OR ST_XMax(Box3D(geom)) > 180 OR ST_YMin(Box3D(geom)) < -90 OR ST_YMax(Box3D(geom)) > 90 ) )::bigint, COALESCE(string_agg(DISTINCT GeometryType(geom), ',' ORDER BY GeometryType(geom)) FILTER (WHERE geom IS NOT NULL), ''), COALESCE(string_agg(DISTINCT ST_SRID(geom)::text, ',' ORDER BY ST_SRID(geom)::text) FILTER (WHERE geom IS NOT NULL), ''), EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = $1 AND indexdef ILIKE '%%USING gist%%' AND indexdef ILIKE '%%geom%%' ) FROM public.%s`, quoteIdentifier(table)) var audit TableAudit var geometryTypes string var srids string audit.Table = table if err := p.pool.QueryRow(ctx, query, table).Scan( &audit.RowCount, &audit.NullGeometryCount, &audit.EmptyGeometryCount, &audit.InvalidCount, &audit.OutOfBoundsCount, &geometryTypes, &srids, &audit.HasGISTIndex, ); err != nil { return TableAudit{}, err } audit.GeometryTypes = splitNonEmpty(geometryTypes) parsedSRIDs, err := parseSRIDs(srids) if err != nil { return TableAudit{}, err } audit.SRIDs = parsedSRIDs return audit, nil } // SearchPlaceCandidates returns bounded name matches from the fixed fire-safety source tables. func (p *PostGIS) SearchPlaceCandidates(ctx context.Context, query domain.PlaceSearchQuery) ([]domain.PlaceCandidate, error) { rows, err := p.pool.Query(ctx, placeCandidateSearchSQL, query.PlaceName, query.Scope.AllTowns, query.Scope.AllowedTowns, query.Limit, ) if err != nil { return nil, err } defer rows.Close() items := make([]domain.PlaceCandidate, 0, query.Limit) for rows.Next() { var item domain.PlaceCandidate if err := rows.Scan( &item.PlaceType, &item.SourceRecordID, &item.Name, &item.Town, &item.Village, &item.MatchedField, &item.MatchedText, &item.MatchKind, &item.Location.Longitude, &item.Location.Latitude, &item.LocationKind, ); err != nil { return nil, err } items = append(items, item) } return items, rows.Err() } // ResolveIncidentContext returns fire grids that cover the supplied point. func (p *PostGIS) ResolveIncidentContext(ctx context.Context, point domain.Coordinate, scope domain.SpatialScope) ([]domain.IncidentContext, error) { rows, err := p.pool.Query(ctx, resolveIncidentContextSQL, point.Longitude, point.Latitude, scope.AllTowns, scope.AllowedTowns) if err != nil { return nil, err } defer rows.Close() items := make([]domain.IncidentContext, 0) for rows.Next() { var item domain.IncidentContext if err := rows.Scan(&item.GridID, &item.Town, &item.AreaLabel); err != nil { return nil, err } items = append(items, item) } return items, rows.Err() } // FindNearbyWaterSources returns water-source and storage-pool candidates. func (p *PostGIS) FindNearbyWaterSources(ctx context.Context, query domain.NearbyQuery) ([]domain.WaterSource, error) { rows, err := p.pool.Query(ctx, nearbyWaterSourcesSQL, query.Point.Longitude, query.Point.Latitude, query.Scope.AllTowns, query.Scope.AllowedTowns, query.RadiusMeters, query.Limit, ) if err != nil { return nil, err } defer rows.Close() items := make([]domain.WaterSource, 0, query.Limit) for rows.Next() { var item domain.WaterSource var name, village, resourceType, reportedStatus pgtype.Text var capacity pgtype.Float8 var sourceTimestamp pgtype.Int8 if err := rows.Scan( &item.Category, &item.SourceRecordID, &name, &item.Town, &village, &item.Location.Longitude, &item.Location.Latitude, &item.DistanceMeters, &capacity, &resourceType, &reportedStatus, &sourceTimestamp, ); err != nil { return nil, err } item.Name = optionalText(name) item.Village = optionalText(village) item.CapacityCubicMeters = optionalFloat64(capacity) item.ResourceType = optionalText(resourceType) item.ReportedStatus = optionalText(reportedStatus) item.SourceTimestampRaw = optionalInt64(sourceTimestamp) items = append(items, item) } return items, rows.Err() } // FindCommandPostCandidates returns nearby check stations and lookout posts. func (p *PostGIS) FindCommandPostCandidates(ctx context.Context, query domain.NearbyQuery) ([]domain.CommandPostCandidate, error) { rows, err := p.pool.Query(ctx, commandPostCandidatesSQL, query.Point.Longitude, query.Point.Latitude, query.Scope.AllTowns, query.Scope.AllowedTowns, query.RadiusMeters, query.Limit, ) if err != nil { return nil, err } defer rows.Close() items := make([]domain.CommandPostCandidate, 0, query.Limit) for rows.Next() { var item domain.CommandPostCandidate var name, village, reportedStatus, managementUnit pgtype.Text if err := rows.Scan( &item.FacilityType, &item.SourceRecordID, &name, &item.Town, &village, &item.Location.Longitude, &item.Location.Latitude, &item.DistanceMeters, &reportedStatus, &managementUnit, ); err != nil { return nil, err } item.Name = optionalText(name) item.Village = optionalText(village) item.ReportedStatus = optionalText(reportedStatus) item.ManagementUnit = optionalText(managementUnit) items = append(items, item) } return items, rows.Err() } // ListNearbyAccessLines returns nearby fire access lines and closest access points. func (p *PostGIS) ListNearbyAccessLines(ctx context.Context, query domain.NearbyQuery) ([]domain.AccessLine, error) { rows, err := p.pool.Query(ctx, nearbyAccessLinesSQL, query.Point.Longitude, query.Point.Latitude, query.Scope.AllTowns, query.Scope.AllowedTowns, query.RadiusMeters, query.Limit, ) if err != nil { return nil, err } defer rows.Close() items := make([]domain.AccessLine, 0, query.Limit) for rows.Next() { var item domain.AccessLine var name, sourceUpdated pgtype.Text var length pgtype.Float8 if err := rows.Scan( &item.SourceRecordID, &item.Town, &name, &item.DistanceMeters, &item.NearestPoint.Longitude, &item.NearestPoint.Latitude, &length, &sourceUpdated, ); err != nil { return nil, err } item.Name = optionalText(name) item.LengthMeters = optionalFloat64(length) item.SourceUpdatedRaw = optionalText(sourceUpdated) items = append(items, item) } return items, rows.Err() } // GetResponsibleUnits returns non-personal responsibility details from covering grids. func (p *PostGIS) GetResponsibleUnits(ctx context.Context, point domain.Coordinate, scope domain.SpatialScope) ([]domain.ResponsibleUnit, error) { rows, err := p.pool.Query(ctx, responsibleUnitsSQL, point.Longitude, point.Latitude, scope.AllTowns, scope.AllowedTowns) if err != nil { return nil, err } defer rows.Close() items := make([]domain.ResponsibleUnit, 0) for rows.Next() { var item domain.ResponsibleUnit if err := rows.Scan(&item.GridID, &item.Town, &item.AreaLabel, &item.FireTeam); err != nil { return nil, err } items = append(items, item) } return items, rows.Err() } // FindNearbyRiskAreas returns nearby cemetery and forest-enterprise polygons. func (p *PostGIS) FindNearbyRiskAreas(ctx context.Context, query domain.NearbyQuery) ([]domain.RiskArea, error) { rows, err := p.pool.Query(ctx, nearbyRiskAreasSQL, query.Point.Longitude, query.Point.Latitude, query.Scope.AllTowns, query.Scope.AllowedTowns, query.RadiusMeters, query.Limit, ) if err != nil { return nil, err } defer rows.Close() items := make([]domain.RiskArea, 0, query.Limit) for rows.Next() { var item domain.RiskArea var name, village, direction pgtype.Text if err := rows.Scan( &item.RiskType, &item.SourceRecordID, &name, &item.Town, &village, &direction, &item.CoversPoint, &item.DistanceMeters, ); err != nil { return nil, err } item.Name = optionalText(name) item.Village = optionalText(village) item.Direction = optionalText(direction) items = append(items, item) } return items, rows.Err() } type spatialTableSpec struct { name string geometryTypes []string } var spatialTableSpecs = []spatialTableSpec{ {name: "st_2_mpslfh_t_slfh_syd", geometryTypes: []string{"POINT"}}, {name: "st_2_xianyouxushuichiguan", geometryTypes: []string{"POINT"}}, {name: "st_2_xianyoufanghuotongdao", geometryTypes: []string{"LINESTRING", "MULTILINESTRING"}}, {name: "st_2_fanghuojianchazhan", geometryTypes: []string{"POINT"}}, {name: "st_2_fanghuoliaowangshao", geometryTypes: []string{"POINT"}}, {name: "st_2_fanghuowangge", geometryTypes: []string{"POLYGON", "MULTIPOLYGON"}}, {name: "st_2_linqugongkuangqiye", geometryTypes: []string{"POLYGON", "MULTIPOLYGON"}}, {name: "st_2_mudifenqu_mian", geometryTypes: []string{"POLYGON", "MULTIPOLYGON"}}, } func quoteIdentifier(identifier string) string { return `"` + strings.ReplaceAll(identifier, `"`, `""`) + `"` } func splitNonEmpty(value string) []string { if value == "" { return []string{} } return strings.Split(value, ",") } func parseSRIDs(value string) ([]int, error) { parts := splitNonEmpty(value) result := make([]int, 0, len(parts)) for _, part := range parts { srid, err := strconv.Atoi(part) if err != nil { return nil, fmt.Errorf("parse geometry SRID: %w", err) } result = append(result, srid) } return result, nil } func optionalText(value pgtype.Text) string { if !value.Valid { return "" } return value.String } func optionalFloat64(value pgtype.Float8) *float64 { if !value.Valid { return nil } result := value.Float64 return &result } func optionalInt64(value pgtype.Int8) *int64 { if !value.Valid { return nil } result := value.Int64 return &result } const placeCandidateSearchSQL = ` WITH candidates AS ( SELECT 'water_source'::text AS place_type, w.objectid::text AS source_record_id, NULLIF(w.name, '') AS name, COALESCE(w.zj, '') AS town, NULL::text AS village, ST_Force2D(w.geom) AS location, 'recorded_point'::text AS location_kind FROM public.st_2_mpslfh_t_slfh_syd AS w WHERE ($2::boolean OR w.zj = ANY($3::text[])) AND w.geom IS NOT NULL AND ST_SRID(w.geom) = 4326 AND GeometryType(w.geom) = 'POINT' AND ST_IsValid(w.geom) AND NOT ST_IsEmpty(w.geom) UNION ALL SELECT 'storage_pool'::text, r.gid::text, NULLIF(r.community, ''), COALESCE(r.auth, ''), NULLIF(r.community, ''), ST_Force2D(r.geom), 'recorded_point'::text FROM public.st_2_xianyouxushuichiguan AS r WHERE ($2::boolean OR r.auth = ANY($3::text[])) AND r.geom IS NOT NULL AND ST_SRID(r.geom) = 4326 AND GeometryType(r.geom) = 'POINT' AND ST_IsValid(r.geom) AND NOT ST_IsEmpty(r.geom) UNION ALL SELECT 'fire_access_line'::text, a.gid::text, NULLIF(a.name, ''), COALESCE(a.auth, ''), NULL::text, ST_StartPoint( CASE WHEN GeometryType(a.geom) = 'LINESTRING' THEN ST_Force2D(a.geom) ELSE ST_GeometryN(ST_Force2D(a.geom), 1) END ), 'representative_point'::text FROM public.st_2_xianyoufanghuotongdao AS a WHERE ($2::boolean OR a.auth = ANY($3::text[])) AND a.geom IS NOT NULL AND ST_SRID(a.geom) = 4326 AND GeometryType(a.geom) IN ('LINESTRING', 'MULTILINESTRING') AND ST_IsValid(a.geom) AND NOT ST_IsEmpty(a.geom) UNION ALL SELECT 'fire_check_station'::text, s.gid::text, NULLIF(s.jczmc, ''), COALESCE(s.zjmc, ''), NULLIF(s.cmc, ''), ST_Force2D(s.geom), 'recorded_point'::text FROM public.st_2_fanghuojianchazhan AS s WHERE ($2::boolean OR s.zjmc = ANY($3::text[])) AND s.geom IS NOT NULL AND ST_SRID(s.geom) = 4326 AND GeometryType(s.geom) = 'POINT' AND ST_IsValid(s.geom) AND NOT ST_IsEmpty(s.geom) UNION ALL SELECT 'fire_lookout'::text, l.gid::text, NULLIF(l.lwsmc, ''), COALESCE(l.zjmc, ''), NULLIF(l.cmc, ''), ST_Force2D(l.geom), 'recorded_point'::text FROM public.st_2_fanghuoliaowangshao AS l WHERE ($2::boolean OR l.zjmc = ANY($3::text[])) AND l.geom IS NOT NULL AND ST_SRID(l.geom) = 4326 AND GeometryType(l.geom) = 'POINT' AND ST_IsValid(l.geom) AND NOT ST_IsEmpty(l.geom) UNION ALL SELECT 'fire_grid'::text, g.gid::text, NULLIF(g.name, ''), COALESCE(g.auth, ''), NULL::text, ST_PointOnSurface(ST_Force2D(g.geom)), 'representative_point'::text FROM public.st_2_fanghuowangge AS g WHERE ($2::boolean OR g.auth = ANY($3::text[])) AND g.geom IS NOT NULL AND ST_SRID(g.geom) = 4326 AND GeometryType(g.geom) IN ('POLYGON', 'MULTIPOLYGON') AND ST_IsValid(g.geom) AND NOT ST_IsEmpty(g.geom) UNION ALL SELECT 'forest_enterprise'::text, e.gid::text, NULLIF(e.lx, ''), COALESCE(e.auth, ''), NULLIF(e.c, ''), ST_PointOnSurface(ST_Force2D(e.geom)), 'representative_point'::text FROM public.st_2_linqugongkuangqiye AS e WHERE ($2::boolean OR e.auth = ANY($3::text[])) AND e.geom IS NOT NULL AND ST_SRID(e.geom) = 4326 AND GeometryType(e.geom) IN ('POLYGON', 'MULTIPOLYGON') AND ST_IsValid(e.geom) AND NOT ST_IsEmpty(e.geom) UNION ALL SELECT 'cemetery_area'::text, c.gid::text, NULLIF(c.name, ''), COALESCE(c.zj, ''), NULLIF(c.cmc, ''), ST_PointOnSurface(ST_Force2D(c.geom)), 'representative_point'::text FROM public.st_2_mudifenqu_mian AS c WHERE ($2::boolean OR c.zj = ANY($3::text[])) AND c.geom IS NOT NULL AND ST_SRID(c.geom) = 4326 AND GeometryType(c.geom) IN ('POLYGON', 'MULTIPOLYGON') AND ST_IsValid(c.geom) AND NOT ST_IsEmpty(c.geom) ), matched AS ( SELECT candidates.*, CASE WHEN lower(COALESCE(name, '')) = lower($1::text) THEN 'name' WHEN lower(COALESCE(village, '')) = lower($1::text) THEN 'village' WHEN lower(COALESCE(town, '')) = lower($1::text) THEN 'town' WHEN strpos(lower(COALESCE(name, '')), lower($1::text)) = 1 THEN 'name' WHEN strpos(lower(COALESCE(village, '')), lower($1::text)) = 1 THEN 'village' WHEN strpos(lower(COALESCE(town, '')), lower($1::text)) = 1 THEN 'town' WHEN strpos(lower(COALESCE(name, '')), lower($1::text)) > 0 THEN 'name' WHEN strpos(lower(COALESCE(village, '')), lower($1::text)) > 0 THEN 'village' ELSE 'town' END AS matched_field, CASE WHEN lower(COALESCE(name, '')) = lower($1::text) THEN 0 WHEN lower(COALESCE(village, '')) = lower($1::text) THEN 1 WHEN lower(COALESCE(town, '')) = lower($1::text) THEN 2 WHEN strpos(lower(COALESCE(name, '')), lower($1::text)) = 1 THEN 3 WHEN strpos(lower(COALESCE(village, '')), lower($1::text)) = 1 THEN 4 WHEN strpos(lower(COALESCE(town, '')), lower($1::text)) = 1 THEN 5 WHEN strpos(lower(COALESCE(name, '')), lower($1::text)) > 0 THEN 6 WHEN strpos(lower(COALESCE(village, '')), lower($1::text)) > 0 THEN 7 ELSE 8 END AS match_rank FROM candidates WHERE strpos(lower(COALESCE(name, '')), lower($1::text)) > 0 OR strpos(lower(COALESCE(village, '')), lower($1::text)) > 0 OR strpos(lower(COALESCE(town, '')), lower($1::text)) > 0 ) SELECT place_type, source_record_id, LEFT(COALESCE(name, ''), 255), LEFT(COALESCE(town, ''), 100), LEFT(COALESCE(village, ''), 255), matched_field, LEFT(CASE matched_field WHEN 'name' THEN name WHEN 'village' THEN village ELSE town END, 255), CASE WHEN match_rank <= 2 THEN 'exact' ELSE 'partial' END, round(ST_X(location)::numeric, 6)::float8, round(ST_Y(location)::numeric, 6)::float8, location_kind FROM matched ORDER BY match_rank, town, village, name, place_type, source_record_id LIMIT $4` const resolveIncidentContextSQL = ` WITH incident AS ( SELECT ST_SetSRID(ST_MakePoint($1, $2), 4326) AS geom ) SELECT g.gid::text, LEFT(COALESCE(g.auth, ''), 100), LEFT(COALESCE(g.name, ''), 255) FROM public.st_2_fanghuowangge AS g CROSS JOIN incident AS i WHERE ($3::boolean OR g.auth = ANY($4::text[])) AND g.geom IS NOT NULL AND ST_SRID(g.geom) = 4326 AND GeometryType(g.geom) IN ('POLYGON', 'MULTIPOLYGON') AND ST_IsValid(g.geom) AND NOT ST_IsEmpty(g.geom) AND ST_Covers(g.geom, i.geom) ORDER BY ST_Area(g.geom::geography), g.gid LIMIT 20` const nearbyWaterSourcesSQL = ` WITH incident AS ( SELECT ST_SetSRID(ST_MakePoint($1, $2), 4326) AS geom ), candidates AS ( SELECT 'water_source'::text AS category, w.objectid::text AS source_record_id, LEFT(NULLIF(w.name, ''), 255) AS name, LEFT(COALESCE(w.zj, ''), 100) AS town, NULL::text AS village, ST_X(w.geom) AS longitude, ST_Y(w.geom) AS latitude, ST_Distance(w.geom::geography, i.geom::geography) AS distance_meters, w.xsrl::float8 AS capacity_cubic_meters, LEFT(NULLIF(w.lx, ''), 100) AS resource_type, LEFT(NULLIF(w.syzt, ''), 100) AS reported_status, w.hc_datetime AS source_timestamp_raw FROM public.st_2_mpslfh_t_slfh_syd AS w CROSS JOIN incident AS i WHERE ($3::boolean OR w.zj = ANY($4::text[])) AND w.geom IS NOT NULL AND ST_SRID(w.geom) = 4326 AND GeometryType(w.geom) = 'POINT' AND ST_IsValid(w.geom) AND NOT ST_IsEmpty(w.geom) AND w.geom && ST_Expand(i.geom, $5 / GREATEST(1000.0, 111320.0 * abs(cos(radians($2))))) AND ST_DWithin(w.geom::geography, i.geom::geography, $5) UNION ALL SELECT 'storage_pool'::text, r.gid::text, LEFT(NULLIF(r.community, ''), 255), LEFT(COALESCE(r.auth, ''), 100), LEFT(NULLIF(r.community, ''), 255), ST_X(r.geom), ST_Y(r.geom), ST_Distance(r.geom::geography, i.geom::geography), r.lfs::float8, LEFT(NULLIF(r.lx, ''), 100), NULL::text, NULL::bigint FROM public.st_2_xianyouxushuichiguan AS r CROSS JOIN incident AS i WHERE ($3::boolean OR r.auth = ANY($4::text[])) AND r.geom IS NOT NULL AND ST_SRID(r.geom) = 4326 AND GeometryType(r.geom) = 'POINT' AND ST_IsValid(r.geom) AND NOT ST_IsEmpty(r.geom) AND r.geom && ST_Expand(i.geom, $5 / GREATEST(1000.0, 111320.0 * abs(cos(radians($2))))) AND ST_DWithin(r.geom::geography, i.geom::geography, $5) ) SELECT category, source_record_id, name, town, village, round(longitude::numeric, 6)::float8, round(latitude::numeric, 6)::float8, round(distance_meters::numeric, 1)::float8, capacity_cubic_meters, resource_type, reported_status, source_timestamp_raw FROM candidates ORDER BY distance_meters, category, source_record_id LIMIT $6` const commandPostCandidatesSQL = ` WITH incident AS ( SELECT ST_SetSRID(ST_MakePoint($1, $2), 4326) AS geom ), candidates AS ( SELECT 'fire_check_station'::text AS facility_type, s.gid::text AS source_record_id, LEFT(NULLIF(s.jczmc, ''), 255) AS name, LEFT(COALESCE(s.zjmc, ''), 100) AS town, LEFT(NULLIF(s.cmc, ''), 255) AS village, ST_X(s.geom) AS longitude, ST_Y(s.geom) AS latitude, ST_Distance(s.geom::geography, i.geom::geography) AS distance_meters, LEFT(NULLIF(s.syzt, ''), 100) AS reported_status, LEFT(NULLIF(s.gldw, ''), 255) AS management_unit FROM public.st_2_fanghuojianchazhan AS s CROSS JOIN incident AS i WHERE ($3::boolean OR s.zjmc = ANY($4::text[])) AND s.geom IS NOT NULL AND ST_SRID(s.geom) = 4326 AND GeometryType(s.geom) = 'POINT' AND ST_IsValid(s.geom) AND NOT ST_IsEmpty(s.geom) AND s.geom && ST_Expand(i.geom, $5 / GREATEST(1000.0, 111320.0 * abs(cos(radians($2))))) AND ST_DWithin(s.geom::geography, i.geom::geography, $5) UNION ALL SELECT 'fire_lookout'::text, l.gid::text, LEFT(NULLIF(l.lwsmc, ''), 255), LEFT(COALESCE(l.zjmc, ''), 100), LEFT(NULLIF(l.cmc, ''), 255), ST_X(l.geom), ST_Y(l.geom), ST_Distance(l.geom::geography, i.geom::geography), LEFT(NULLIF(l.syzt, ''), 100), NULL::text FROM public.st_2_fanghuoliaowangshao AS l CROSS JOIN incident AS i WHERE ($3::boolean OR l.zjmc = ANY($4::text[])) AND l.geom IS NOT NULL AND ST_SRID(l.geom) = 4326 AND GeometryType(l.geom) = 'POINT' AND ST_IsValid(l.geom) AND NOT ST_IsEmpty(l.geom) AND l.geom && ST_Expand(i.geom, $5 / GREATEST(1000.0, 111320.0 * abs(cos(radians($2))))) AND ST_DWithin(l.geom::geography, i.geom::geography, $5) ) SELECT facility_type, source_record_id, name, town, village, round(longitude::numeric, 6)::float8, round(latitude::numeric, 6)::float8, round(distance_meters::numeric, 1)::float8, reported_status, management_unit FROM candidates ORDER BY distance_meters, facility_type, source_record_id LIMIT $6` const nearbyAccessLinesSQL = ` WITH incident AS ( SELECT ST_SetSRID(ST_MakePoint($1, $2), 4326) AS geom ), candidates AS ( SELECT a.gid::text AS source_record_id, LEFT(COALESCE(a.auth, ''), 100) AS town, LEFT(NULLIF(a.name, ''), 255) AS name, ST_Distance(a.geom::geography, i.geom::geography) AS distance_meters, ST_ClosestPoint(a.geom, i.geom) AS nearest_point, ST_Length(a.geom::geography) AS length_meters, a.sttime::text AS source_updated_raw FROM public.st_2_xianyoufanghuotongdao AS a CROSS JOIN incident AS i WHERE ($3::boolean OR a.auth = ANY($4::text[])) AND a.geom IS NOT NULL AND ST_SRID(a.geom) = 4326 AND GeometryType(a.geom) IN ('LINESTRING', 'MULTILINESTRING') AND ST_IsValid(a.geom) AND NOT ST_IsEmpty(a.geom) AND a.geom && ST_Expand(i.geom, $5 / GREATEST(1000.0, 111320.0 * abs(cos(radians($2))))) AND ST_DWithin(a.geom::geography, i.geom::geography, $5) ) SELECT source_record_id, town, name, round(distance_meters::numeric, 1)::float8, round(ST_X(nearest_point)::numeric, 6)::float8, round(ST_Y(nearest_point)::numeric, 6)::float8, length_meters, source_updated_raw FROM candidates ORDER BY distance_meters, source_record_id LIMIT $6` const responsibleUnitsSQL = ` WITH incident AS ( SELECT ST_SetSRID(ST_MakePoint($1, $2), 4326) AS geom ) SELECT g.gid::text, LEFT(COALESCE(g.auth, ''), 100), LEFT(COALESCE(g.name, ''), 255), LEFT(COALESCE(g.fhzd, ''), 255) FROM public.st_2_fanghuowangge AS g CROSS JOIN incident AS i WHERE ($3::boolean OR g.auth = ANY($4::text[])) AND g.geom IS NOT NULL AND ST_SRID(g.geom) = 4326 AND GeometryType(g.geom) IN ('POLYGON', 'MULTIPOLYGON') AND ST_IsValid(g.geom) AND NOT ST_IsEmpty(g.geom) AND ST_Covers(g.geom, i.geom) ORDER BY ST_Area(g.geom::geography), g.gid LIMIT 20` const nearbyRiskAreasSQL = ` WITH incident AS ( SELECT ST_SetSRID(ST_MakePoint($1, $2), 4326) AS geom ), candidates AS ( SELECT 'cemetery_area'::text AS risk_type, c.gid::text AS source_record_id, LEFT(NULLIF(c.name, ''), 255) AS name, LEFT(COALESCE(c.zj, ''), 100) AS town, LEFT(NULLIF(c.cmc, ''), 255) AS village, LEFT(NULLIF(c.fw, ''), 255) AS direction, ST_Covers(c.geom, i.geom) AS covers_point, ST_Distance(c.geom::geography, i.geom::geography) AS distance_meters FROM public.st_2_mudifenqu_mian AS c CROSS JOIN incident AS i WHERE ($3::boolean OR c.zj = ANY($4::text[])) AND c.geom IS NOT NULL AND ST_SRID(c.geom) = 4326 AND GeometryType(c.geom) IN ('POLYGON', 'MULTIPOLYGON') AND ST_IsValid(c.geom) AND NOT ST_IsEmpty(c.geom) AND c.geom && ST_Expand(i.geom, $5 / GREATEST(1000.0, 111320.0 * abs(cos(radians($2))))) AND ST_DWithin(c.geom::geography, i.geom::geography, $5) UNION ALL SELECT 'forest_enterprise'::text, e.gid::text, LEFT(NULLIF(e.lx, ''), 255), LEFT(COALESCE(e.auth, ''), 100), LEFT(NULLIF(e.c, ''), 255), LEFT(NULLIF(e.fw, ''), 255), ST_Covers(e.geom, i.geom), ST_Distance(e.geom::geography, i.geom::geography) FROM public.st_2_linqugongkuangqiye AS e CROSS JOIN incident AS i WHERE ($3::boolean OR e.auth = ANY($4::text[])) AND e.geom IS NOT NULL AND ST_SRID(e.geom) = 4326 AND GeometryType(e.geom) IN ('POLYGON', 'MULTIPOLYGON') AND ST_IsValid(e.geom) AND NOT ST_IsEmpty(e.geom) AND e.geom && ST_Expand(i.geom, $5 / GREATEST(1000.0, 111320.0 * abs(cos(radians($2))))) AND ST_DWithin(e.geom::geography, i.geom::geography, $5) ) SELECT risk_type, source_record_id, name, town, village, direction, covers_point, round(distance_meters::numeric, 1)::float8 FROM candidates ORDER BY covers_point DESC, distance_meters, risk_type, source_record_id LIMIT $6`