• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Freegle / Iznik / 21438

15 Jun 2026 08:00AM UTC coverage: 70.75% (-0.5%) from 71.2%
21438

push

circleci

edwh
docs(spatial): complete spatial-server docs + plain-English volunteer overview

Documentation for the spatial subsystem merged in #459:

- iznik-spatial-go/README.md (NEW): the KNN "finder" service had no README at
  all. Documents all six datasets, every public + admin endpoint, env vars,
  the nightly/15-min sync scheduler, SQLite index persistence, Docker wiring,
  callers, and the /swagger OpenAPI generation.
- iznik-routing-go/README.md: completeness pass — adds the five live endpoints
  that were undocumented (/v1/ripple-schedule, POST /v1/ripple-eval,
  /v1/posts-for-member, /v1/digest-simulator, /swagger), the
  ROUTING_DRIVE_SPEED_FACTOR env var, the JWT-on-external-port note, and a
  cross-link to the spatial service.
- SPATIAL-SERVERS.md (NEW, repo root): a non-technical, plain-English overview
  for volunteers/moderators/staff — what the two services do ("finder" +
  "travel-time mapper"), why they exist, fairness, and the rippling-out idea —
  with no code, linking down to the technical READMEs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

10952 of 14552 branches covered (75.26%)

Branch coverage included in aggregate %.

118160 of 167938 relevant lines covered (70.36%)

35.47 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

70.18
/iznik-batch/app/Services/PostcodeRemapService.php
1
<?php
2

3
namespace App\Services;
4

5
use Illuminate\Support\Facades\DB;
6
use Illuminate\Support\Facades\Http;
7
use Illuminate\Support\Facades\Log;
8

9
/**
10
 * Remaps postcodes to their nearest enclosing area using the spatial server.
11
 *
12
 * When a location area's geometry is created or modified, postcodes within
13
 * that geometry need to be reassigned to the correct (smallest, nearest) area.
14
 * This mirrors the V1 PHP Location::remapPostcodes() logic.
15
 *
16
 * Queries the iznik-spatial-go service (HTTP) instead of PostgreSQL/PostGIS.
17
 * The spatial server maintains its own SQLite R-tree index built from MySQL
18
 * and uses the same 12-level progressive buffer expansion algorithm as V1.
19
 */
20
class PostcodeRemapService
21
{
22
    private string $spatialServerUrl;
23

24
    public function __construct()
6✔
25
    {
26
        $this->spatialServerUrl = config('freegle.spatial_server_url', 'http://localhost:8194');
6✔
27
    }
28

29
    /**
30
     * Remap postcodes within a given WKT polygon to their nearest area.
31
     *
32
     * @param int|null $locationId The location that was modified (unused, kept for interface compatibility).
33
     * @param string|null $polygon WKT polygon to scope the remap. NULL = remap all.
34
     * @return int Number of postcodes remapped.
35
     */
36
    public function remapPostcodes(?int $locationId = NULL, ?string $polygon = NULL): int
2✔
37
    {
38
        $pcQuery = DB::table('locations_spatial')
2✔
39
            ->join('locations', 'locations_spatial.locationid', '=', 'locations.id')
2✔
40
            ->where('locations.type', 'Postcode')
2✔
41
            ->whereRaw("LOCATE(' ', locations.name) > 0")
2✔
42
            ->select(
2✔
43
                'locations.id as locations_id',
2✔
44
                'locations_spatial.locationid',
2✔
45
                'locations.name',
2✔
46
                'locations.lat',
2✔
47
                'locations.lng',
2✔
48
                'locations.areaid',
2✔
49
            );
2✔
50

51
        if ($polygon) {
2✔
52
            $srid = (int) config('freegle.srid', 3857);
1✔
53
            if ($locationId) {
1✔
54
                $pcQuery->where(function ($q) use ($polygon, $locationId, $srid) {
×
55
                    $q->whereRaw(
×
56
                        "ST_Contains(ST_GeomFromText(?, {$srid}), locations_spatial.geometry)",
×
57
                        [$polygon],
×
58
                    )->orWhere('locations.areaid', $locationId);
×
59
                });
×
60
            } else {
61
                $pcQuery->whereRaw(
1✔
62
                    "ST_Contains(ST_GeomFromText(?, {$srid}), locations_spatial.geometry)",
1✔
63
                    [$polygon],
1✔
64
                );
1✔
65
            }
66
        }
67

68
        $count   = 0;
2✔
69
        $updated = 0;
2✔
70

71
        $pcQuery->orderBy('locations.id')->chunkById(1000, function ($postcodes) use (&$count, &$updated) {
2✔
72
            foreach ($postcodes as $pc) {
×
73
                $newAreaId = $this->findNearestArea($pc->lng, $pc->lat);
×
74

75
                if ($newAreaId && $newAreaId != $pc->areaid) {
×
76
                    DB::update('UPDATE locations SET areaid = ? WHERE id = ?', [
×
77
                        $newAreaId,
×
78
                        $pc->locationid,
×
79
                    ]);
×
80
                    $updated++;
×
81
                }
82

83
                $count++;
×
84

85
                if ($count % 1000 === 0) {
×
86
                    Log::info("PostcodeRemapService: processed {$count}, updated {$updated}");
×
87
                }
88
            }
89
        }, 'locations.id', 'locations_id');
2✔
90

91
        Log::info("PostcodeRemapService: remapped {$updated}/{$count} postcodes");
2✔
92

93
        return $updated;
2✔
94
    }
95

96
    /**
97
     * Find the nearest area for a given point via the spatial server.
98
     *
99
     * @param float $lng Longitude
100
     * @param float $lat Latitude
101
     * @return int|null Location ID of the best matching area, or null.
102
     */
103
    public function findNearestArea(float $lng, float $lat): ?int
4✔
104
    {
105
        // type=Area restricts the expanding-buffer search to non-postcode
106
        // locations, matching the V1 PostGIS candidate pool (which synced
107
        // everything except postcodes).
108
        $response = Http::get("{$this->spatialServerUrl}/v1/locations/knn", [
4✔
109
            'lng'   => $lng,
4✔
110
            'lat'   => $lat,
4✔
111
            'limit' => 1,
4✔
112
            'type'  => 'Area',
4✔
113
        ]);
4✔
114

115
        if ($response->successful()) {
4✔
116
            $results = $response->json('results', []);
3✔
117
            return !empty($results) ? (int) $results[0]['id'] : null;
3✔
118
        }
119

120
        Log::warning("PostcodeRemapService: spatial server returned {$response->status()} for lng={$lng} lat={$lat}");
1✔
121
        return null;
1✔
122
    }
123
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc