# Delivery address from the map (website)

The website now adds an address the same way the app does: map first, then a short form.
The workflow and the matching rules come from the app developer's
`MAPS_BACKEND_WORKFLOW.md` (Flutter, `place_to_address.dart`).

## Deploying

**Before (Google Cloud Console, the key in `MAP_API_KEY`):**

1. **Restrict the key.**
   - Application restrictions: "Websites", `kapitano.shop/*` and `*.kapitano.shop/*`.
   - API restrictions: Maps JavaScript API, Geocoding API, Places API.

   This key appears in the page source, as every Maps key does, so without restrictions anyone
   can use it on our bill.
2. **Check the APIs.** The key needs **Maps JavaScript API**, **Geocoding API**, and
   **Places API** (the legacy one is enough; the new one is used when enabled).

**On the server:**

```
git pull
php artisan migrate
```

The migration adds two indexes (cities by state, states by country). It runs in under a
second, touches no data, and is safe to run again. Nothing changes for visitors yet.

**To turn the map on**, add this to `.env`:

```
ADDRESS_MAP_PICKER=true
```

Also make sure `MAP_API_KEY` is set. If the server caches its configuration, run
`php artisan config:cache` afterwards.

**To turn it off at once**, remove the line or set it to `false`, then run `config:cache`
again if used. The old address forms are back; no code goes back.

## When it is on

It is on only when all three are set:

| Setting | Where |
|---|---|
| `ADDRESS_MAP_PICKER=true` | `.env` |
| `MAP_API_KEY=...` | `.env` |
| "Google Map" on | Admin, Setup → Google → Google Map (already on, live) |

While it is off, every address form is exactly the old one.

## What the customer sees

1. "Add new address" (profile, dashboard, checkout) opens the map, with:
   - a fixed pin
   - search
   - "my location"
   - a **satellite** button
   - "Confirm location"
2. The short form follows:
   - "Location set" with the area, and "Change location"
   - the address text, filled in and editable
   - phone (the account's number) and notes
3. The country, state and city are chosen automatically. Whatever cannot be matched is
   picked from our lists, with "Could not match the state".
4. **Guests:** a "Pick the location on the map" button in the checkout form fills in the
   address, the state and the city (the delivery price updates), and the coordinates. The
   map is optional, as in the app.
5. **No "Edit" on addresses** while the picker is on: the app removed editing too. Delete
   and "make default" stay.

## Location accuracy

- **The GPS (the device's own, free)** keeps refining for up to 20 s until it is within 15 m.
  The pin follows every better fix.
- **A blue dot and a circle** show where the GPS puts the customer and how wide its error is,
  with "دقة الموقع: ± 8 متر" under the map.
- **Above 50 m**, the customer is asked to move the pin to the door. A computer has no GPS
  (its location comes from Wi-Fi or the IP address, often hundreds of metres off); a phone
  is accurate to a few metres.
- **The GPS stops the moment the customer moves the map**, so it never pulls the pin back.
- **The map zooms to the building** (zoom 18) when the fix is good.
- **Satellite view:** the same map with photos and street names on top, so the customer
  finds the building. It is not closer than zoom 17: Google has no photos closer than that
  for parts of Syria.
- **What customers need on their side:** location services on, and the browser allowed
  "precise location".

## What it costs at Google

| Action | Google requests |
|---|---|
| Opening a page | 0. The Maps script loads the first time the map is opened. |
| Opening the map | 1 map load per page, whatever the number of openings |
| Moving the map, the pin, satellite | 0 (the same map) |
| "My location" | 0 (the device's GPS) |
| Search | Autocomplete 600 ms after typing stops, one session per search, then the place's location only |
| **"Confirm location"** | **1 Geocoding request** |

The profile page and the customer dashboard no longer load the old map script while the
picker is on. That script found no map on those pages and loaded Google on every visit.

## How the city is chosen (same as the app)

`App\Services\AddressMap\GooglePlaceResolver` + `PlaceNameMatcher`:

- **result:** of Google's answers, the first street address is used, not a shop or landmark
  that users added (those can carry names like "Gouvernorat de Damas").
- **country:** by ISO code, then by name.
- **state:** `administrative_area_level_1` (for example "محافظة دمشق").
- **city:** the first one that matches, in this order: `locality`,
  `administrative_area_level_2`, `sublocality_level_1`, `sublocality`, `neighborhood`,
  `administrative_area_level_3`, then the state's own name.
- **Each level is looked up like the app's API calls:** first the names containing Google's
  name (`LIKE`), then the whole list.
- **Names are normalized** before comparing:
  - tashkeel and invisible marks removed
  - أ/إ/آ → ا, ة → ه, ى → ي
  - "ال" and words like "محافظة" or "منطقة" dropped
- **Matching:** an exact match first, else the containing name of closest length.

Known behaviour, also in the app: the city named "المحافظة" normalizes to nothing, so it
contains every name and wins in its state whenever nothing matches exactly (for example
Kobani becomes "المحافظة").

Checked against live data: of 60 addresses the app saved from the map, the website picks the
same state and city for 58. The other 2 were typed by hand, and the customer chose the
neighbourhood in the app.

## When Google fails

| What fails | What happens |
|---|---|
| Key refused (expired, quota, wrong domain) | The site detects it (`gm_authFailure`, or Google's error on the map), says so, and opens the **old form**. |
| Script does not load (blocked, no answer in 20 s) | Same. |
| No answer to "Confirm" within 10 s, or nothing found | The customer picks the state and city from the lists. The location is kept. |
| Our lists do not load (network) | The form says so. |

The customer can always add an address.

## Checking the matching on real data

```
php artisan address-map:check --limit=50
```

This takes addresses the app saved from the map, asks Google for the same points again,
runs the website's matching, and prints how many get the same state and city as the app
did. It is read only, but makes one Geocoding request per address.

A key restricted to the website's domain is refused for server calls. Pass a server key with
`--key=...` in that case.

## Security fix shipped with it

An address could be read, changed, deleted, made default or delivered to by **any** signed-in
user who knew its id. Now only its owner can:

- **website:** `addresses.edit` / `update` / `destroy` / `set_default`, and `addresses.store`
  with `customer_id` (staff only)
- **seller panel:** the same four actions
- **app API:**
  - `user/shipping/update`, `update-location` and `make_default` answer
    `{"result": false, "message": "Address not found"}`, the same as `delete` already did
  - `update-address-in-cart` checks that the address belongs to `user_id`, and that a signed-in
    caller is that user. `address_id: 0` still works.
- **checkout:** `delivery-info` and `updateDeliveryAddress`.

The app's own calls are unchanged (tested with the exact payloads from the app's workflow).

## Tests

- **PHP:** `AddressMapResolverTest`, `AddressMapEndpointsTest`, `AddressOwnershipTest`.
- **The script itself:** `tests/js/address_map_picker.test.mjs`.
  - It runs the real script against a fake browser, Google, GPS and clock: cost, GPS,
    accuracy, satellite, search, confirm, the form, fallbacks and guests.
  - Run it with `node --test tests/js/address_map_picker.test.mjs`. It needs Node only, no
    packages.
  - `AddressMapScriptTest` runs it with the PHP suite.

## Files

| What | Where |
|---|---|
| Endpoints | `AddressMapController`, routes `address_map.resolve` (guests too, 60/min per IP, since carriers share IPs) and `address_map.store` (signed in, 20/min) |
| Matching | `app/Services/AddressMap/*` |
| Screens | `resources/views/frontend/partials/address/map_picker/{modal,script}.blade.php` |
| Indexes | `2026_10_05_000002_add_state_and_country_indexes_to_cities_and_states` |
| Check on real data | `php artisan address-map:check` |
