1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
|
<?php
declare(strict_types=1);
namespace Cepis;
/**
* The CEP's coordinate, when the database has one.
*
* The point is the **median** of the coordinates of that CEP's addresses, not
* the mean: one badly geocoded address 40 km away shifts a mean and does not
* move a median.
*
* Always check {@see Geo::$precisao} before using the coordinate for distance:
*
* - `"logradouro"`: the point is a stretch of street. `raio_m` lands around
* 100–150 m. Good for maps and for distance.
* - `"localidade"`: the CEP *is* the whole municipality (a city-wide general
* CEP), and the point is its centroid. `raio_m` runs from 7 km to 40 km.
* Computing shipping from this is wrong by tens of kilometres.
*
* `raio_m` is the radius containing 90% of that CEP's addresses. It exists
* precisely so that error cannot pass unnoticed.
*/
final class Geo
{
/**
* Property names are the API's own JSON keys, `raio_m` included. See
* {@see Address} for why they are not translated or camel-cased.
*
* @param string $precisao Kept a plain string rather than an enum so a
* precision level the API adds later cannot break this client.
*/
public function __construct(
public readonly float $lat,
public readonly float $lon,
public readonly int $raio_m,
public readonly string $precisao,
) {
}
/**
* Build from the decoded `geo` block, or `null` when it is missing or
* malformed.
*
* A malformed `geo` is not worth losing the whole address over, since the
* rest of the response is still useful.
*/
public static function fromArray(mixed $data): ?self
{
if (!is_array($data)) {
return null;
}
foreach (['lat', 'lon', 'raio_m', 'precisao'] as $key) {
if (!isset($data[$key]) || !is_scalar($data[$key])) {
return null;
}
}
if (!is_numeric($data['lat']) || !is_numeric($data['lon']) || !is_numeric($data['raio_m'])) {
return null;
}
return new self(
lat: (float) $data['lat'],
lon: (float) $data['lon'],
raio_m: (int) (float) $data['raio_m'],
precisao: (string) $data['precisao'],
);
}
/**
* `true` when the coordinate points at a street, not a municipality.
*
* Shorthand for `precisao === "logradouro"`, which is the check to make
* before using the point to measure distance.
*/
public function isPrecise(): bool
{
return $this->precisao === 'logradouro';
}
/**
* @return array{lat: float, lon: float, raio_m: int, precisao: string}
*/
public function toArray(): array
{
return [
'lat' => $this->lat,
'lon' => $this->lon,
'raio_m' => $this->raio_m,
'precisao' => $this->precisao,
];
}
}
|