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

Freegle / Iznik / 26023

10 Jul 2026 07:36AM UTC coverage: 72.165%. Remained the same
26023

push

circleci

edwh
docs(mobile): drop dead V1 API references (V1 is retired)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L3VgD1vna2q5BYQYwnxiaT

12944 of 17053 branches covered (75.9%)

Branch coverage included in aggregate %.

135384 of 188487 relevant lines covered (71.83%)

38.03 hits per line

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

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

3
namespace App\Services;
4

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

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

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

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

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

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

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

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

84
                $count++;
×
85

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

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

94
        return $updated;
2✔
95
    }
96

97
    /**
98
     * Find the nearest area for a given point via the spatial server.
99
     *
100
     * @param float $lng Longitude
101
     * @param float $lat Latitude
102
     * @return int|null Location ID of the best matching area, or null.
103
     */
104
    public function findNearestArea(float $lng, float $lat): ?int
5✔
105
    {
106
        // type=Area restricts the expanding-buffer search to non-postcode
107
        // locations, matching the V1 PostGIS candidate pool (which synced
108
        // everything except postcodes).
109
        try {
110
            $response = Http::get("{$this->spatialServerUrl}/v1/locations/knn", [
5✔
111
                'lng'   => $lng,
5✔
112
                'lat'   => $lat,
5✔
113
                'limit' => 1,
5✔
114
                'type'  => 'Area',
5✔
115
            ]);
5✔
116
        } catch (ConnectionException $e) {
1✔
117
            // A transient blip on the spatial server (e.g. mid-rebuild) must not
118
            // abort the whole remap. Treat it like a failed lookup: skip this
119
            // postcode (return null => no update) so the run continues.
120
            Log::warning("PostcodeRemapService: spatial server unreachable for lng={$lng} lat={$lat}: {$e->getMessage()}");
1✔
121
            return null;
1✔
122
        }
123

124
        if ($response->successful()) {
4✔
125
            $results = $response->json('results', []);
3✔
126
            return !empty($results) ? (int) $results[0]['id'] : null;
3✔
127
        }
128

129
        Log::warning("PostcodeRemapService: spatial server returned {$response->status()} for lng={$lng} lat={$lat}");
1✔
130
        return null;
1✔
131
    }
132
}
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