update-municipal-contacts · git:20260528.a073e6c · 2026-05-28 · sha256 b757dc8e2a545b97

update-municipal-contacts git:20260528.a073e6cA

Immutable. This exact content never changes and is served at /api/v1/blob/b757dc8e2a545b97.

---
name: update-municipal-contacts
description: Refresh the per-município mayor email contacts (data/officials/municipal_contacts/index.json) by scraping the iisda.government.bg "Кметове на общини" registry. Use when the daily watch report flags `iisda_mayors` as changed, when the user asks to refresh mayor contacts / municipal contacts / kmet emails, after a local-election or chmi cycle when new mayors take office, or after a fresh git clone if data/officials/municipal_contacts/index.json carries an empty `contactsByObshtina`.
allowed-tools:
  - Read
  - Bash
  - Edit
  - Write
---

# Update municipal contacts skill

Refreshes `data/officials/municipal_contacts/index.json` — the per-municipality mayor email contact list that powers the My-Area dashboard's "Контакти на общината" tile and the "Свържете се" button on each mayor card. Covers 260 of Bulgaria's 265 municipalities; Sofia city contributes once under the synthetic `SOF00` code that fans out to the 24 районы via the hook fallback.

Source contract:

| Field | Source | Granularity | Cadence |
|---|---|---|---|
| `email`, `mayor`, `iisda_id` | [iisda.government.bg `governing_body/<id>` detail pages](https://iisda.government.bg/ras/governing_bodies/gb_municipality_administrations) — IDs in the contiguous block 4400..4950 | per municipality (obshtina code) | shifts on regular & partial local-election cycles |

`scripts/officials/municipal_contacts/scrape_iisda.ts` walks the ID range serially with a 200 ms polite delay, caches each detail page to `raw_data/officials/iisda_mayors/<id>.html`, filters for pages whose body contains `Кмет на община` AND `Общинска администрация`, extracts the first email address + município name, and name-matches against `data/municipalities.json` with two manual aliases (`Добричка` → `DOB15`, `Столична община` → `SOF00`).

## When to run

| Trigger | Action |
|---|---|
| Daily watcher reports `iisda.government.bg — Кметове на общини` as changed | Step 1 |
| User asks to "refresh mayor contacts" / "update municipal contacts" / "rescrape iisda" | Step 1 |
| New regular-local cycle (mi*) or chmi cycle has finalized — new mayors took office | Step 1 with `--force` to refetch HTML |
| Fresh clone with empty `contactsByObshtina` | Step 1 |

The iisda registry is dormant between election cycles; routine fingerprint flips are usually individual chmi (частични избори) replacements, not bulk changes.

## Step 1 — Scrape

```bash
npx tsx scripts/officials/municipal_contacts/scrape_iisda.ts          # uses cached HTML when present
```

Force a full re-fetch (after a regular-local cycle):

```bash
rm -rf raw_data/officials/iisda_mayors
npx tsx scripts/officials/municipal_contacts/scrape_iisda.ts          # ~2 min cold
```

Expected output on a routine run:

```
  scanned 100/551 · mayors=50 · matched=50
  scanned 200/551 · mayors=100 · matched=99
  scanned 300/551 · mayors=150 · matched=149
  ...
  scanned 550/551 · mayors=260 · matched=260

Done. Wrote .../data/officials/municipal_contacts/index.json — 260/260 mayors matched, 0 unmatched.
```

## Step 2 — Spot-check

Cross-check the new entries against the email domains — they should align with the município's name conventions (`kmet@varna.bg`, `obshtina@burgas.bg`, etc.):

```bash
python3 -c "
import json
c = json.load(open('data/officials/municipal_contacts/index.json'))
samples = list(c['contactsByObshtina'].items())[:5]
for code, x in samples:
    print(f'  {code}: {x.get(\"email\")}  (iisda_id={x.get(\"iisda_id\")})')
print(f'total: {len(c[\"contactsByObshtina\"])} municipalities')
"
```

Sanity checks:
- Total ≈ 260 (5 short of 265: Sofia city is one record under SOF00; 4 other gaps mean the iisda ID range needs widening — see "Known limitations").
- BGS04 → `@burgas.bg`, RSE27 → `@ruse-bg.eu`, SOF00 → `@sofia.bg`.

If the unmatched-mayors line shows > 5, the name → obshtina-code join broke. Look at the printed "Unmatched" sample and either widen `MANUAL_ALIASES` in the script or extend the matching heuristics.

## Step 3 — Commit + bucket sync

```bash
git add data/officials/municipal_contacts/index.json
git commit -m "officials: refresh municipal mayor contacts from iisda"
npm run bucket:sync:dry
npm run bucket:sync
```

## Step 4 — Stamp success

```bash
npx tsx scripts/stamp-ingest.ts update-municipal-contacts \
  --summary "260/260 mayors matched · 0 unmatched"
```

Only stamp on the public data-changes log when emails actually changed:

```bash
if [ -n "$(git diff --stat data/officials/municipal_contacts/)" ]; then
  npx tsx scripts/append-data-change.ts update-municipal-contacts \
    --summary "260/260 mayors matched · 0 unmatched" \
    --source "iisda.government.bg — Кметове на общини"
fi
```

## Known limitations

- **iisda only exposes email** — phone, website, postal address, working hours are not on the detail pages (only declaratively in the page text). The tile renders email + tel: link if a phone shows up under a future scrape extension.
- **Sofia city contributes one record** under SOF00; the 24 районы see it via `useMunicipalContacts`' SOF00 fallback.
- The **ID range 4400..4950 is hand-chosen** based on the observed mayor-ID distribution. If iisda re-allocates IDs after a major government re-organisation, widen the range (`ID_RANGE_START` / `ID_RANGE_END` in the script) and re-run with `--force`.

## What this skill does NOT do

- **Does not refresh кметство (village-mayor) contacts.** Sub-municipal village mayors are scraped from CIK local-elections HTML by `update-local-elections` and lifted into `data/local_mayors/`; this skill is only the município-level mayor.
- **Does not touch the кметство → EKATTE lookup.** That's a `update-local-elections` derived step (`scripts/parsers_local/backfill_kmetstvo_ekatte.ts`).
- **Does not stamp the public data-changes log** when only the `scrapedAt` timestamp moved.

## File map

| Path | Purpose |
|---|---|
| `scripts/officials/municipal_contacts/scrape_iisda.ts` | CLI entry — walk iisda IDs, extract emails |
| `raw_data/officials/iisda_mayors/<id>.html` | cached detail-page HTML (skip-on-exist) |
| `data/officials/municipal_contacts/index.json` | output — `contactsByObshtina` map |
| `scripts/watch/sources/iisda_mayors.ts` | watcher — fingerprints pagination count + first-page IDs |
| `src/data/officials/useMunicipalContacts.tsx` | React Query hook with Sofia район → SOF00 fallback |
| `src/screens/myarea/MyAreaContactsTile.tsx` | "Контакти на общината" tile |