summaryrefslogtreecommitdiffstats
path: root/src/Geo.php
diff options
context:
space:
mode:
Diffstat (limited to 'src/Geo.php')
-rw-r--r--src/Geo.php94
1 files changed, 94 insertions, 0 deletions
diff --git a/src/Geo.php b/src/Geo.php
new file mode 100644
index 0000000..c677622
--- /dev/null
+++ b/src/Geo.php
@@ -0,0 +1,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,
+ ];
+ }
+}