> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# البحث عن العناوين Geocoding مع Nominatim
- URL: https://arabroot.io/articles/البحث-عن-العناوين-geocoding-مع-nominatim/
- Published: 2026-10-09T10:45:00.000Z
- Updated: 2026-10-09T13:01:19.000Z
- Description: كيف تحول اسم المكان إلى إحداثيات والإحداثيات إلى عنوان على سيرفرك مع Nominatim، وما الذي يجيده بالعربية وما الذي يفوته.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, مجالات متخصصة, تطبيقات لفريقك, قواعد البيانات, الاستضافة الذاتية

في مقال [خرائطك على سيرفرك مع OpenStreetMap وMapLibre](https://arabroot.io/articles/%D8%AE%D8%B1%D8%A7%D8%A6%D8%B7%D9%83-%D8%B9%D9%84%D9%89-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D9%83-%D9%85%D8%B9-openstreetmap-%D9%88-maplibre/) وضعنا خريطة الرياض كاملة على سيرفرنا، ولكن الخريطة وحدها لا تكفي أغلب التطبيقات، فالمستخدم يكتب «مستشفى الملك فيصل التخصصي» ويريد أن يراه على الخريطة، والمندوب يرسل إحداثيات GPS من هاتفه وتريد أن تعرف في أي حي هو. وهذه العملية اسمها البحث عن العناوين Geocoding، والطريقة البسيطة هي أن ترسل كل بحث إلى Google أو إلى السيرفر العام لمشروع OpenStreetMap، ولكن الأول يحسب عليك كل طلب ويرى كل عنوان يبحث عنه عملاؤك، والثاني له سياسة استخدام صارمة سوف نقرؤها بعد قليل.

والحل الذي نشرحه هنا هو [Nominatim](https://github.com/osm-search/Nominatim?ref=arabroot.io)، وهو البرنامج المفتوح المصدر نفسه الذي يشغل البحث في موقع OpenStreetMap، بترخيص GPL، على سيرفرك وبياناتك. وهو المقال الثاني في مرحلة [الخدمات المتخصصة: الخرائط والبث المباشر ومعالجة الوسائط](https://arabroot.io/start/#stage-special)، وسوف نناقش فيه ما يلي:

- ما هو البحث عن العناوين Geocoding والبحث العكسي Reverse Geocoding.
- كيف يعمل Nominatim من الداخل، ولماذا يحتاج استيراداً طويلاً مرة واحدة.
- سياسة استخدام السيرفر العام، ومتى تحتاج سيرفرك الخاص.
- مختبر كامل على Docker: استيراد مستطيل الرياض، مع قياس الوقت والذاكرة، وأسئلة بالعربية والإنجليزية بمخرجاتها الحقيقية.
- ما يجيده Nominatim بالعربية وما يفوته، وربطه بالخريطة من المقال السابق.

## ما هو البحث عن العناوين Geocoding؟

البحث عن العناوين Geocoding هو تحويل نص يكتبه إنسان، اسم مكان أو عنوان، إلى إحداثيات خط العرض Latitude وخط الطول Longitude، والبحث العكسي Reverse Geocoding هو العكس: تعطيه نقطة فيعيد لك أقرب عنوان لها. والصورة التالية تبين الاتجاهين بإجابتين حقيقيتين من المختبر:

![مخطط من صفين: الأول البحث، المستخدم يكتب مستشفى الملك فيصل التخصصي فيرد Nominatim بخط العرض 24.6707344 وخط الطول 46.6745034 من نوع amenity hospital لاستخدامها في علامة على الخريطة أو منطقة توصيل أو مسافة. والثاني البحث العكسي، نقطة 24.7114 و46.6744 من GPS الهاتف أو نقرة على الخريطة فيرد Nominatim بطريق الملك فهد الفرعي، الورود، الرياض، منطقة الرياض، 12215، السعودية، وهو أقرب جسم في OSM وهو مكتب داخل مركز المملكة، ومع zoom 18 مبنى و14 حي و10 مدينة](https://arabroot.io/content/images/2026/10/nominatim-01-geocoding.webp)

البحث يحول النص إلى نقطة، والبحث العكسي يحول النقطة إلى أقرب عنوان في البيانات

ولاحظ في المثال الثاني أن البحث العكسي لا يخترع عنواناً، وإنما يعيد أقرب جسم مناسب موجود في البيانات، و[توثيق البحث العكسي](https://nominatim.org/release-docs/latest/api/Reverse/?ref=arabroot.io) يقول ذلك صراحة، وينبه إلى أن هذا قد يعطي نتائج غير متوقعة في المدن المزدحمة، وسوف نرى ذلك بأعيننا في المختبر.

## كيف يعمل Nominatim من الداخل؟

Nominatim ليس خدمة تبحث في ملف OpenStreetMap عند كل طلب، والسبب أن البحث في ملايين العناصر وبناء عنوان كل عنصر من الحدود الإدارية حوله عمل ثقيل، لذلك يقسم العمل إلى مرحلتين: استيراد مرة واحدة يبني كل شيء في قاعدة البيانات، ثم أسئلة سريعة على ما بني، والصورة التالية تبين المرحلتين:

![مخطط من صفين: عند الاستيراد، ملف riyadh.osm.pbf بحجم 9.6 ميغابايت ومليون نقطة يدخل osm2pgsql في 31 ثانية إلى PostgreSQL مع PostGIS في جدول placex فيه 100378 مكاناً بحجم 432 ميغابايت، ثم الفهرسة من الرتبة 1 إلى 30 تبني عنوان كل مكان من المناطق حوله، المكان داخل الحي داخل المدينة داخل الدولة، في قرابة أربع دقائق ونصف. وعند كل طلب، التطبيق يرسل search إلى Nominatim الذي يوحد شكل الكلمات ويجد الأماكن التي فيها كل الكلمات ويرتبها بالأهمية ونوع المكان، ثم يرد JSON فيه الإحداثيات والعنوان وسطر رخصة ODbL، في 25 إلى 35 مللي ثانية بعد تسخين الذاكرة](https://arabroot.io/content/images/2026/10/nominatim-02-inside.webp)

الاستيراد يبني العناوين مرة واحدة، والبحث بعد ذلك سؤال سريع لقاعدة البيانات

1. **الاستيراد Import:** أداة [osm2pgsql](https://osm2pgsql.org/?ref=arabroot.io) تقرأ ملف OpenStreetMap بصيغة PBF وتكتبه في PostgreSQL مع إضافة PostGIS، وهي الإضافة الجغرافية التي شرحناها في قسم [PostGIS في مقال PostgreSQL لكل شيء](https://arabroot.io/articles/postgresql-%D9%84%D9%83%D9%84-%D8%B4%D9%8A%D8%A1-%D9%85%D8%AA%D9%89-%D8%AA%D9%83%D9%81%D9%8A%D9%83-%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D9%88%D8%A7%D8%AD%D8%AF%D8%A9/#postgis).
2. **الفهرسة Indexing:** لكل مكان رتبة Rank من 1 للدولة إلى 30 للمبنى والنقطة، وNominatim يمر على الرتب بالترتيب، ويحسب لكل مكان المناطق التي تحتويه، فيعرف أن المحل في حي العليا داخل مدينة الرياض داخل منطقة الرياض، وهذه أطول خطوة في الاستيراد.
3. **البحث Search:** عند كل طلب يوحد شكل الكلمات Normalization، كما يشرح [توثيق ال Tokenizer](https://nominatim.org/release-docs/latest/customize/Tokenizers/?ref=arabroot.io)، ثم يجد الأماكن التي تحمل كل الكلمات، ويرتبها، ويعيد النتيجة JSON مع سطر الرخصة.

وهذا التقسيم يشرح أرقام [متطلبات التثبيت](https://nominatim.org/release-docs/latest/admin/Installation/?ref=arabroot.io): ذاكرة 2 جيجابايت على الأقل وإلا فشل التثبيت، أما العالم كله فيوصي التوثيق بذاكرة 128 جيجابايت أو أكثر ومساحة 1 تيرابايت على الأقل بأقراص NVMe، والاستيراد يأخذ قرابة يومين ونصف على جهاز جيد، وواقعياً أربعة إلى خمسة أيام بأقراص SSD عادية. ولهذا السبب فإن السؤال الصحيح ليس «هل أستضيف Nominatim؟» وإنما «ما المنطقة التي يحتاجها تطبيقي؟».

## السيرفر العام: متى يكفي ومتى لا يكفي؟

مشروع OpenStreetMap يشغل سيرفراً عاماً لـ Nominatim، و[سياسة استخدامه](https://operations.osmfoundation.org/policies/nominatim/?ref=arabroot.io) واضحة:

- طلب واحد في الثانية كحد أقصى مطلق، ولا يسمح بالاستخدام الثقيل.
- يجب أن ترسل ترويسة User-Agent أو Referer تعرف تطبيقك، والقيمة الافتراضية لمكتبات HTTP لا تقبل.
- يجب أن تخزن النتائج عندك Cache، والكلاينت الذي يكرر السؤال نفسه قد يعتبر معطوباً ويحجب.
- السكربتات التي تعمل أكثر من يوم أو تعمل بشكل دوري محدودة بأربعة طلبات في الدقيقة.
- الإكمال التلقائي Autocomplete أثناء الكتابة ممنوع.
- يجب أن تعرض نسبة البيانات Attribution، والتعريف بالاستخدام الثقيل قد يتغير ويمكن أن يسحب منك الوصول.

لذلك يكفيك السيرفر العام لتجربة أو لأداة داخلية تبحث عن عناوين قليلة يكتبها إنسان، أما إذا كان البحث جزءاً من تطبيقك، أو عندك ملف آلاف العناوين تريد تحويله دفعة واحدة Batch Geocoding، أو لا تريد أن تخرج عناوين عملائك من شبكتك، فأنت تحتاج سيرفرك الخاص.

## التجربة العملية: Nominatim لمدينة الرياض

سوف نستورد مستطيل الرياض نفسه الذي استخدمناه في مقال الخرائط، حتى تكون الخريطة والبحث على البيانات نفسها، ونقيس الوقت والذاكرة على جهازنا، وهي أرقام لهذا الجهاز وهذه البيانات فقط وليست وعداً لجهازك.

### تجهيز البيانات

شركة Geofabrik تنشر مقتطفات OpenStreetMap حسب المناطق، والسعودية فيها ضمن ملف [دول مجلس التعاون الخليجي](https://download.geofabrik.de/asia/gcc-states.html?ref=arabroot.io)، والصفحة تذكر أن الرخصة ODbL 1.0، وأن الملفات لا تحتوي أسماء المستخدمين ومعرفاتهم بسبب قوانين حماية البيانات الأوروبية:

```bash
mkdir -p nominatim-lab/data && cd nominatim-lab
curl -fsSLO https://download.geofabrik.de/asia/gcc-states-latest.osm.pbf
curl -fsSLO https://download.geofabrik.de/asia/gcc-states-latest.osm.pbf.md5
md5sum -c gcc-states-latest.osm.pbf.md5
ls -l gcc-states-latest.osm.pbf | awk '{print $5, $9}'
```

والمخرج سوف يكون كما يلي:

```text
gcc-states-latest.osm.pbf: OK
254703997 gcc-states-latest.osm.pbf
```

الملف 255 ميغابايت لست دول، ونحن نحتاج الرياض فقط، فنقصها بأداة [osmium](https://osmcode.org/osmium-tool/manual.html?ref=arabroot.io) من Container مؤقت على Debian:

```bash
docker run --rm --name lab-maps-osmium -v "$PWD:/osm" debian:13.1 bash -c "
  apt-get update -qq >/dev/null && apt-get install -y -qq osmium-tool >/dev/null 2>&1 &&
  osmium --version | head -1 &&
  time osmium extract --bbox 46.45,24.45,47.05,25.00 --strategy smart -S types=multipolygon,boundary \
    /osm/gcc-states-latest.osm.pbf -o /osm/data/riyadh.osm.pbf --overwrite &&
  osmium fileinfo -e /osm/data/riyadh.osm.pbf | grep -E 'Number of (nodes|ways|relations)'"
ls -l data/riyadh.osm.pbf | awk '{print $5, $9}'
```

والمخرج سوف يكون كما يلي:

```text
osmium version 1.18.0

real	0m30.481s
user	0m27.053s
sys	0m7.650s
  Number of nodes: 1000654
  Number of ways: 217148
  Number of relations: 681
9555328 data/riyadh.osm.pbf
```

في الإعداد أعلاه لاحظ التالي:

- المستطيل هو نفسه في مقال الخرائط، بالترتيب: الغرب، الجنوب، الشرق، الشمال.
- الاستراتيجية `smart` كما في [توثيق osmium extract](https://docs.osmcode.org/osmium/latest/osmium-extract.html?ref=arabroot.io) تكمل الطرق والعلاقات التي تقطع حدود المستطيل، وأضفنا `boundary` إلى أنواعها حتى تأتي الحدود الإدارية كاملة، لأن Nominatim يبني عنوان كل مكان منها، فإذا قطعت حدود منطقة الرياض في منتصفها فسوف تخرج العناوين ناقصة.
- النتيجة ملف 9.6 ميغابايت فيه مليون نقطة تقريباً و217 ألف طريق ومضلع، ونحذف ملف الخليج بعد ذلك إذا لم نعد نحتاجه.

### ملف التشغيل

نستخدم Docker Image من مشروع [nominatim-docker](https://github.com/mediagis/nominatim-docker?ref=arabroot.io)، وهي Image واحدة فيها Nominatim وPostgreSQL معاً، ومثبتة على الإصدار `5.3.2`، وخياراتها في [دليل الاستخدام](https://github.com/mediagis/nominatim-docker/blob/master/howto.md?ref=arabroot.io). والملف `compose.yaml`:

```yaml
name: lab-maps-nominatim
services:
  nominatim:
    image: mediagis/nominatim:5.3.2
    container_name: lab-maps-nominatim
    shm_size: 1g
    environment:
      PBF_PATH: /nominatim/data/riyadh.osm.pbf
      IMPORT_STYLE: full
      THREADS: 4
      NOMINATIM_PASSWORD: change-me-lab-only
      POSTGRES_SHARED_BUFFERS: 1GB
      POSTGRES_MAINTENANCE_WORK_MEM: 2GB
      POSTGRES_EFFECTIVE_CACHE_SIZE: 4GB
    volumes:
      - ./data:/nominatim/data
      - lab-maps-nominatim-data:/var/lib/postgresql/16/main
    ports:
      - "127.0.0.1:18710:8080"

volumes:
  lab-maps-nominatim-data:
    name: lab-maps-nominatim-data
```

في الإعداد أعلاه لاحظ التالي:

- `PBF_PATH` يستورد الملف المحلي، والبديل `PBF_URL` يحمله بنفسه، ولا يجتمعان.
- `IMPORT_STYLE: full` هو الافتراضي ويستورد العناوين ونقاط الاهتمام Points of Interest مثل المستشفيات والمحلات، والأنماط الأصغر `admin` و`street` و`address` توفر المساحة إذا كنت تحتاج المدن أو الشوارع فقط.
- القيم الافتراضية لذاكرة PostgreSQL في هذه ال Image كبيرة، منها `POSTGRES_MAINTENANCE_WORK_MEM` بقيمة 10GB و`POSTGRES_EFFECTIVE_CACHE_SIZE` بقيمة 24GB كما في دليل الاستخدام، لذلك خفضناها إلى ما يناسب مدينة واحدة على سيرفر صغير.
- المنفذ 8080 هو واجهة API، وربطناه ب 127.0.0.1 فقط، ولم ننشر منفذ PostgreSQL 5432 إطلاقاً، وغير `NOMINATIM_PASSWORD` قبل أي استخدام حقيقي.
- ال Volume على مجلد قاعدة البيانات يحفظ الاستيراد، فإذا أعدت إنشاء ال Container فلن يستورد من جديد.

### الاستيراد: الوقت والذاكرة

نشغل ال Container بسكربت صغير `measure.sh` يسجل استهلاك الذاكرة من `docker stats` كل بضع ثوان حتى يجيب API:

```bash
#!/bin/bash
cd "$(dirname "$0")"
log=measure.log; : > $log
start=$(date +%s); echo "start $(date -u +%T)" >> $log
docker compose up -d >/dev/null 2>&1
until curl -sf "http://127.0.0.1:18710/status?format=json" >/dev/null 2>&1; do
  m=$(docker stats --no-stream --format '{{.MemUsage}}' lab-maps-nominatim | awk '{print $1}')
  echo "$(date -u +%T) $m" >> $log
  sleep 2
done
end=$(date +%s); echo "ready $(date -u +%T) seconds=$((end-start))" >> $log
```

ثم نقرأ السجل بعد أن ينتهي:

```bash
chmod +x measure.sh && ./measure.sh
head -1 measure.log; tail -1 measure.log; grep -c MiB measure.log
awk 'NF==2 && $2 ~ /MiB$/ {n=$2+0; if (n>m) {m=n; t=$1}} END {print "max", m, "MiB at", t}' measure.log
```

والمخرج سوف يكون كما يلي:

```text
start 20:27:07
ready 20:33:43 seconds=396
103
max 703.9 MiB at 20:30:36
```

أي أن الاستيراد كاملاً أخذ 6 دقائق و36 ثانية، وأعلى استهلاك للذاكرة في 103 قراءات هو 703.9 ميغابايت أثناء الفهرسة، وقد يكون الأعلى الحقيقي أكبر قليلاً بين قراءتين. وفي سجلات ال Container تفاصيل المراحل:

```bash
docker logs lab-maps-nominatim 2>&1 | grep -E "Importing OSM data file|osm2pgsql took|Starting indexing rank \(1|FINISHED rank 26|Warming database caches|Application startup complete" | head -6
```

```text
2026-10-08 20:27:21: Importing OSM data file
2026-10-08 20:27:52  osm2pgsql took 31s overall.
2026-10-08 20:28:12: Starting indexing rank (1 to 4) using 4 threads
2026-10-08 20:31:05: Done 79390/79394 in 157 @ 505.059 per second - FINISHED rank 26
2026-10-08 20:32:39: Warming database caches
[2026-10-08 20:33:43 +0000] [365] [INFO] Application startup complete.
```

في المخرج أعلاه لاحظ التالي:

- osm2pgsql أخذ 31 ثانية فقط، والوقت الحقيقي في الفهرسة التي بدأت 20:28:12 وانتهت قبل 20:32:39، أي قرابة أربع دقائق ونصف، منها دقيقتان ونصف للرتبة 26 وحدها، وهي رتبة الشوارع، وعددها في الرياض 79,390.
- بعد الفهرسة يسخن Nominatim ذاكرة قاعدة البيانات Warming دقيقة تقريباً، ثم يبدأ API.

وفي السجل نفسه تحذير مهم:

```text
Wikipedia/Wikidata importance tables missing.
Quality of search results may be degraded. Reverse geocoding is unaffected.
See https://nominatim.org/release-docs/latest/admin/Import/#wikipediawikidata-rankings
```

والسبب أننا لم نستورد بيانات الأهمية من ويكيبيديا، وهي التي تجعل «الرياض» المدينة تظهر قبل مطعم اسمه الرياض، وتفعيلها بالمتغير `IMPORT_WIKIPEDIA` كما في [توثيق الاستيراد](https://nominatim.org/release-docs/latest/admin/Import/?ref=arabroot.io)، وسوف نرى أثر غيابها في ترتيب بعض النتائج. ثم نتحقق من الحجم وعدد الأماكن:

```bash
docker exec lab-maps-nominatim du -sh /var/lib/postgresql/16/main
docker exec lab-maps-nominatim sudo -u postgres psql -d nominatim -tAc \
  "select pg_size_pretty(pg_database_size('nominatim')), (select count(*) from placex), (select count(*) from placex where housenumber is not null)"
```

والمخرج سوف يكون كما يلي:

```text
1.1G	/var/lib/postgresql/16/main
432 MB|100378|435
```

قاعدة البيانات 432 ميغابايت ومجلد PostgreSQL كاملاً 1.1 جيجابايت من ملف حجمه 9.6 ميغابايت، أي أن المساحة على القرص أكثر من مئة ضعف حجم الملف المضغوط، فخطط للقرص على هذا الأساس. ولاحظ الرقم الأخير: من 100,378 مكاناً في الرياض هناك 435 فقط عليها رقم مبنى Housenumber في OpenStreetMap، لذلك لا تتوقع أن يجد Nominatim عنواناً بمستوى رقم المبنى في الرياض، وإنما الشارع والحي والمكان المعروف.

### البحث بالعربية والإنجليزية

نبدأ بحالة الخدمة:

```bash
curl -s "http://127.0.0.1:18710/status?format=json"
```

```text
{"status":0,"message":"OK","data_updated":"2026-10-07T10:45:17+00:00","software_version":"5.3.2","database_version":"5.3.2-0"}
```

ثم أول بحث بالعربية، ونطلب نتيجة واحدة بصيغة `jsonv2`:

```bash
curl -s -G http://127.0.0.1:18710/search --data-urlencode "q=حديقة الملك سلمان" -d format=jsonv2 -d limit=1 \
  | python3 -m json.tool --no-ensure-ascii --indent 2
```

والمخرج سوف يكون كما يلي:

```json
[
  {
    "place_id": 140558,
    "licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright",
    "osm_type": "relation",
    "osm_id": 13254453,
    "lat": "24.7219400",
    "lon": "46.7173087",
    "category": "boundary",
    "type": "administrative",
    "place_rank": 20,
    "importance": 0.1333433333333333,
    "addresstype": "neighbourhood",
    "name": "حديقة الملك سلمان",
    "display_name": "حديقة الملك سلمان, محافظة الرياض, منطقة الرياض, السعودية",
    "boundingbox": [
      "24.6935546",
      "24.7496418",
      "46.6929688",
      "46.7465649"
    ]
  }
]
```

في المخرج أعلاه لاحظ التالي:

- `--data-urlencode` يرمز النص العربي في الرابط، ولا تكتب الحروف العربية في الرابط مباشرة.
- النتيجة الأولى حد إداري `boundary` باسم حديقة الملك سلمان من رتبة الحي، وليست الحديقة نفسها، فالبيانات فيها الاثنان، وسوف تظهر الحديقة `leisure/park` في البحث التالي.
- سطر `licence` يذكرك أن النتائج من بيانات ODbL، فإذا عرضتها في تطبيقك فاعرض النسبة كما في مقال الخرائط.

ولتسهيل قراءة بقية النتائج نعرف دالة صغيرة تطبع النوع والعنوان فقط:

```bash
names() { python3 -c 'import json,sys; [print(r["type"], "|", r["display_name"]) for r in json.load(sys.stdin)]'; }
curl -s -G http://127.0.0.1:18710/search --data-urlencode "q=King Salman Park" -d format=jsonv2 | names
curl -s -G http://127.0.0.1:18710/search --data-urlencode "q=King Salman Park" -d format=jsonv2 -d accept-language=en | names
curl -s -G http://127.0.0.1:18710/search --data-urlencode "q=مطار الملك خالد الدولي" -d format=jsonv2 -d accept-language=en | names
```

والمخرج سوف يكون كما يلي:

```text
park | حديقة الملك سلمان, الرياض, محافظة الرياض, منطقة الرياض, 11461, السعودية
park | منتزة الملك سلمان البري, محافظة الرياض, منطقة الرياض, السعودية
park | King Salman Park, Riyadh, Riyadh governorate, Riyadh Region, 11461, Saudi Arabia
park | King Salman Park, Riyadh governorate, Riyadh Region, Saudi Arabia
aerodrome | King Khalid International Airport, Riyadh governorate, Riyadh Region, Saudi Arabia
```

وهنا نقطة تحير كثيراً من المطورين: بحثنا بالإنجليزية فجاءت النتائج بالعربية، والسبب كما في [توثيق البحث](https://nominatim.org/release-docs/latest/api/Search/?ref=arabroot.io) أن لغة النتيجة تحددها ترويسة `Accept-Language` أو المعامل `accept-language`، وأدوات سطر الأوامر مثل curl لا ترسل الترويسة، فيعرض Nominatim الاسم المحلي، وهو العربي في الرياض. أما المتصفح فيرسلها تلقائياً بلغة جهاز المستخدم، لذلك حدد اللغة صراحة في تطبيقك. ولاحظ أن البحث يقبل لغة ويعيد بلغة أخرى، فالسؤال بالعربية عن المطار أعاد العنوان بالإنجليزية.

### ما يجيده بالعربية وما يفوته

```bash
curl -s -G http://127.0.0.1:18710/search --data-urlencode "q=برج المملكة" -d format=jsonv2 | names
curl -s -G http://127.0.0.1:18710/search --data-urlencode "q=مركز المملكة" -d format=jsonv2 | names
curl -s -G http://127.0.0.1:18710/search --data-urlencode "q=طريق الأمير محمد بن عبدالعزيز" -d format=jsonv2 -d limit=2 | names
curl -s -G http://127.0.0.1:18710/search --data-urlencode "q=حديقه الملك سلمان" -d format=jsonv2 | names
```

والمخرج سوف يكون كما يلي:

```text
mall | مركز المملكة, سيدة الرؤساء, الرياض, محافظة الرياض, منطقة الرياض, 12251, السعودية
commercial | مركز المملكة, رقم 94, العليا, الرياض, محافظة الرياض, منطقة الرياض, 12244, السعودية
restaurant | Macboos Restaurant, التحليه مع الملك فهد, طريق الامير محمد بن عبدالعزيز, Riyadh Saudi Arabia, طريق الملك فهد, بلدية المعذر, الرياض, محافظة الرياض, منطقة الرياض, 12313, السعودية
restaurant | TOPKAPI Tahlia, التحليه مع الملك فهد, طريق الامير محمد بن عبدالعزيز, Riyadh Saudi Arabia, طريق الملك فهد, بلدية المعذر, الرياض, محافظة الرياض, منطقة الرياض, 12313, السعودية
```

أربعة أسئلة وأربع نتائج مختلفة، وكل واحدة تعلمك شيئاً:

- **«برج المملكة» لم يعد شيئاً:** لأن الاسم في البيانات «مركز المملكة»، وNominatim يبحث في الأسماء الموجودة في OpenStreetMap ومنها الأسماء البديلة `alt_name` إن وجدت، ولا يعرف الأسماء الدارجة التي يستخدمها الناس، والحل في البيانات لا في الكود.
- **الهمزة لا تهم:** كتبنا «الأمير» بالهمزة، والاسم في البيانات «الامير» دونها، ومع ذلك وجده، لأن Nominatim يوحد أشكال الألف قبل البحث.
- **الترتيب ليس دائماً ما تتوقع:** سألنا عن الطريق فجاء مطعمان على الطريق قبله، وهذا من أثر غياب بيانات الأهمية من ويكيبيديا، ومن عناوين أدخلها مستخدمون بصيغة غير مرتبة مثل «Riyadh Saudi Arabia» داخل حقل الشارع.
- **التاء المربوطة والهاء مختلفتان:** «حديقه» بالهاء لم تعد شيئاً، مع أنها من أشهر أخطاء الكتابة عند المستخدمين، فإذا كان تطبيقك يعتمد على ما يكتبه الناس بسرعة فعالج هذا في تطبيقك، مثلاً بإعادة السؤال بالتاء المربوطة إذا لم تأت نتيجة.

وللعناوين المنظمة، مثل نموذج فيه حقل للشارع وحقل للمدينة، يوجد البحث المنظم Structured Search، وهو لا يجتمع مع `q` كما في التوثيق:

```bash
curl -s -G http://127.0.0.1:18710/search --data-urlencode "street=طريق العليا" --data-urlencode "city=الرياض" \
  -d format=jsonv2 -d limit=3 | names
```

```text
secondary | مدخل طريق العليا, الملك فهد, الرياض, محافظة الرياض, منطقة الرياض, 12361, السعودية
tertiary | طريق العليا العام, السلامة, العليا, الرياض, محافظة الرياض, منطقة الرياض, 12212, السعودية
secondary | مدخل طريق العليا, الورود, الرياض, محافظة الرياض, منطقة الرياض, 12361, السعودية
```

### البحث العكسي

نعطيه إحداثيات مركز المملكة ونطلب تفاصيل العنوان:

```bash
curl -s -G http://127.0.0.1:18710/reverse -d lat=24.7114 -d lon=46.6744 -d format=jsonv2 -d addressdetails=1 \
  | python3 -m json.tool --no-ensure-ascii --indent 2
```

والمخرج سوف يكون كما يلي:

```json
{
  "place_id": 145026,
  "licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright",
  "osm_type": "node",
  "osm_id": 6535480762,
  "lat": "24.7111809",
  "lon": "46.6737654",
  "category": "office",
  "type": "company",
  "place_rank": 30,
  "importance": 9.99999999995449e-06,
  "addresstype": "office",
  "name": "McKinsey & Company",
  "display_name": "McKinsey & Company, طريق الملك فهد الفرعي, الورود, الرياض, محافظة الرياض, منطقة الرياض, 12215, السعودية",
  "address": {
    "office": "McKinsey & Company",
    "road": "طريق الملك فهد الفرعي",
    "suburb": "الورود",
    "city": "الرياض",
    "province": "محافظة الرياض",
    "state": "منطقة الرياض",
    "ISO3166-2-lvl4": "SA-01",
    "postcode": "12215",
    "country": "السعودية",
    "country_code": "sa"
  },
  "boundingbox": [
    "24.7111309",
    "24.7112309",
    "46.6737154",
    "46.6738154"
  ]
}
```

النقطة داخل مركز المملكة، والنتيجة مكتب شركة داخل المبنى، لأن البحث العكسي يعيد أقرب جسم في البيانات، وهو هنا نقطة المكتب وليس المبنى. والحقل `address` هو الأهم لتطبيقك، فيه الشارع والحي والمدينة والرمز البريدي كل في حقل مستقل، فلا تقسم `display_name` بالفواصل. وإذا كنت تحتاج الحي أو المدينة فقط فاستخدم المعامل `zoom`:

```bash
for z in 18 14 10; do
  curl -s -G http://127.0.0.1:18710/reverse -d lat=24.7114 -d lon=46.6744 -d zoom=$z -d format=jsonv2 -d accept-language=en \
    | python3 -c 'import json,sys; r=json.load(sys.stdin); print(r["addresstype"], "|", r["display_name"])'
done
```

```text
office | McKinsey & Company, King Fahad Branch Road, Al Wuroud District, Riyadh, Riyadh governorate, Riyadh Region, 12215, Saudi Arabia
suburb | Al `Ulayya, Riyadh, Riyadh governorate, Riyadh Region, 12212, Saudi Arabia
city | Riyadh, Riyadh governorate, Riyadh Region, 12634, Saudi Arabia
```

وجدول [توثيق البحث العكسي](https://nominatim.org/release-docs/latest/api/Reverse/?ref=arabroot.io) يحدد المعنى: 18 للمبنى، و14 للحي Neighbourhood، و10 للمدينة، و3 للدولة. ولاحظ أن المبنى في حي الورود والحي في المستوى 14 هو العليا، لأن النقطة على حدود الحيين، وهذا ما قصده التوثيق بالنتائج غير المتوقعة في المدن المزدحمة. وفي الطلبات المتكررة على السيرفر نفسه كان متوسط زمن 50 طلب بحث عكسي 33.5 مللي ثانية وأقصاها 90 مللي ثانية، وهي أرقام توضيحية على جهاز المختبر.

### البحث على الخريطة

الآن نربط Nominatim بخريطة المقال السابق، فنضيف صفحة `search.html` إلى مجلد `web` نفسه، فيها حقل بحث ونقرة على الخريطة للبحث العكسي:

```html
<!doctype html>
<html lang="ar" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>البحث عن عنوان</title>
<link rel="stylesheet" href="/lib/maplibre-gl.css">
<style>
html, body, #map { margin: 0; height: 100%; font-family: system-ui, sans-serif; }
form { position: absolute; top: 12px; right: 12px; z-index: 1; display: flex; gap: 6px; background: #fff; padding: 8px; border-radius: 10px; box-shadow: 0 2px 8px #0003; }
input { width: 300px; font-size: 16px; padding: 6px 10px; }
.maplibregl-popup-content { font-size: 14px; max-width: 320px; }
</style>
<script type="importmap">{ "imports": { "fflate": "/lib/fflate.mjs" } }</script>
</head>
<body>
<form id="f"><input id="q" placeholder="ابحث عن مكان أو عنوان" autocomplete="off"><button>بحث</button></form>
<div id="map"></div>
<script type="module">
import * as maplibregl from '/lib/maplibre-gl.mjs';
import { Protocol } from '/lib/pmtiles.mjs';
import { layers, namedFlavor } from '/lib/basemaps.mjs';

const NOMINATIM = 'http://127.0.0.1:18710';
maplibregl.addProtocol('pmtiles', new Protocol().tile);
const map = window.map = new maplibregl.Map({
  container: 'map', center: [46.6753, 24.7136], zoom: 12,
  style: { version: 8, glyphs: '/assets/fonts/{fontstack}/{range}.pbf', sprite: `${location.origin}/assets/sprites/v4/light`,
    sources: { protomaps: { type: 'vector', url: 'pmtiles:///tiles/riyadh.pmtiles',
      attribution: '<a href="https://www.openstreetmap.org/copyright">© OpenStreetMap</a>' } },
    layers: layers('protomaps', namedFlavor('light'), { lang: 'ar' }) }
});
const marker = new maplibregl.Marker({ color: '#E95420' });
const show = (lon, lat, text) => {
  marker.setLngLat([lon, lat]).setPopup(new maplibregl.Popup({ offset: 30 }).setText(text)).addTo(map).togglePopup();
};

// search: text -> coordinates (search is sent only on submit, never on every key press)
document.getElementById('f').addEventListener('submit', async (e) => {
  e.preventDefault();
  const q = document.getElementById('q').value.trim();
  if (!q) return;
  const r = await fetch(`${NOMINATIM}/search?` + new URLSearchParams({ q, format: 'jsonv2', limit: 1, 'accept-language': 'ar' }));
  const [hit] = await r.json();
  if (!hit) return alert('لا توجد نتيجة');
  map.flyTo({ center: [+hit.lon, +hit.lat], zoom: 16, duration: 0 });
  show(+hit.lon, +hit.lat, hit.display_name);
});

// reverse: a click on the map -> the nearest address
map.on('click', async (e) => {
  const { lng, lat } = e.lngLat;
  const r = await fetch(`${NOMINATIM}/reverse?` + new URLSearchParams({ lat, lon: lng, format: 'jsonv2', zoom: 18, 'accept-language': 'ar' }));
  const hit = await r.json();
  show(lng, lat, hit.display_name || 'لا يوجد عنوان هنا');
});
</script>
</body>
</html>
```

في الإعداد أعلاه لاحظ التالي:

- الصفحة على المنفذ 18701 وNominatim على 18710، أي على أصلين Origins مختلفين، وهذا يعمل لأن Nominatim يرد بالترويسة `access-control-allow-origin: *`، وفي الإنتاج ضعهما خلف دومين واحد بمسارين عبر Reverse Proxy.
- البحث يرسل عند الضغط على الزر فقط وليس مع كل حرف، فإذا أردت إكمالاً تلقائياً أثناء الكتابة فهذا ليس عمل Nominatim، وسوف نعود إليه بعد قليل.
- نحدد `accept-language` صراحة حتى لا تتغير لغة النتائج بحسب جهاز المستخدم.

والصورة التالية بعد البحث عن «مستشفى الملك فيصل التخصصي»:

![صفحة البحث وفي أعلاها حقل فيه مستشفى الملك فيصل التخصصي وزر بحث، والخريطة مقربة على مجمع المستشفى بين شارع الزهراوي وطريق التخصصي، وعلامة برتقالية فوقها نافذة فيها: مستشفى الملك فيصل التخصصي، الأحنف بن قيس، المؤتمرات، بلدية الشميسي، الرياض، محافظة الرياض، منطقة الرياض، 12713، السعودية](https://arabroot.io/content/images/2026/10/nominatim-03-search.webp)

البحث بالعربية من سيرفرنا، والخريطة من سيرفرنا أيضاً

وهذه بعد نقرة على مركز المملكة، وفيها ما رأيناه في سطر الأوامر: أقرب جسم هو المكتب لا المبنى:

![الخريطة مقربة على مركز المملكة بين طريق الملك فهد وشارع العليا، وعلامة برتقالية في وسطه فوقها نافذة: McKinsey & Company، طريق الملك فهد الفرعي، الورود، الرياض، محافظة الرياض، منطقة الرياض، 12215، السعودية](https://arabroot.io/content/images/2026/10/nominatim-04-reverse.webp)

البحث العكسي بنقرة على الخريطة يعيد أقرب جسم في البيانات

## نكسر الإعداد: سؤال خارج المنطقة

ماذا يحدث إذا سأل المستخدم عن مكان خارج المنطقة التي استوردتها؟

```bash
curl -s -G http://127.0.0.1:18710/search --data-urlencode "q=كورنيش جدة" -d format=jsonv2; echo
curl -s -G http://127.0.0.1:18710/reverse -d lat=21.5433 -d lon=39.1728 -d format=jsonv2 \
  | python3 -c 'import json,sys; r=json.load(sys.stdin); print(r["addresstype"], "|", r["display_name"])'
```

والمخرج سوف يكون كما يلي:

```text
[]
country | السعودية
```

البحث أعاد قائمة فارغة، وهذا متوقع، ولكن انتبه للسطر الثاني: نقطة في جدة أعادت «السعودية» دون أي خطأ، والسبب أن حدود الدولة كاملة في ملفنا بفضل استراتيجية `smart`، فالنقطة داخل الدولة ولا يوجد حولها أي شيء آخر. وإذا كان تطبيقك يخزن هذه النتيجة كعنوان العميل فسوف تجد في قاعدة بياناتك عناوين اسمها «السعودية» فقط، لذلك تحقق من `addresstype` أو `place_rank` في الرد، وارفض النتيجة إذا كانت أعم مما تحتاج.

## من المختبر إلى الإنتاج

- **المنطقة:** ابدأ من المنطقة التي يحتاجها تطبيقك، مدينة أو دولة، فالذاكرة والقرص ووقت الاستيراد تكبر معها، والعالم كله مشروع مختلف تماماً كما رأينا في المتطلبات.
- **التحديث:** Geofabrik تنشر تحديثات يومية لملف الخليج في [gcc-states-updates](https://download.geofabrik.de/asia/gcc-states-updates/?ref=arabroot.io)، وال Image تقبلها بالمتغير `REPLICATION_URL` و`UPDATE_MODE`، ولكن التحديثات تخص المنطقة كاملة، فإذا قصصت مستطيلاً كما فعلنا فالأبسط أن تعيد الاستيراد دورياً، أو أن تستورد ملف المنطقة كاملاً وتحدثه.
- **جودة الترتيب:** فعل `IMPORT_WIKIPEDIA` في الإنتاج، فهو يحسن ترتيب الأماكن المعروفة، ولا يؤثر في البحث العكسي كما يقول التحذير نفسه.
- **الإكمال التلقائي:** Nominatim لا يدعم البحث أثناء الكتابة، والسيرفر العام يمنعه صراحة، وإذا احتجته فمشروع [Photon](https://github.com/komoot/photon?ref=arabroot.io) بترخيص Apache 2.0 مبني لهذا الغرض ويستورد بياناته من Nominatim.
- **الأمان:** لا تنشر المنفذ 5432، وضع API خلف Reverse Proxy بحد لعدد الطلبات Rate Limiting، والسبب أن كل سؤال ثقيل على قاعدة البيانات، وسكربت واحد يمكن أن يشغل السيرفر كله.
- **النسخ الاحتياطي:** لا تنسخ قاعدة Nominatim كما تنسخ بيانات تطبيقك، لأنها تبنى من جديد من ملف OSM، واحفظ بدلاً منها ملف `compose.yaml` وتاريخ البيانات التي استوردتها.
- **النسبة:** النتائج من بيانات ODbL، فاعرض نسبة OpenStreetMap حيث تعرض النتائج، و[إرشادات النسبة](https://osmfoundation.org/wiki/Licence/Attribution%5FGuidelines?ref=arabroot.io) تقول إن مجموعة نتائج البحث لا يلزم أن تحمل النسبة مع كل نتيجة ما لم تشكل قاعدة بيانات مشتقة.

## تنظيف المختبر

من داخل مجلد المختبر نحذف ال Container والشبكة وال Volume بالاسم، ثم Docker Image وملفات البيانات:

```bash
docker compose down -v
docker image rm mediagis/nominatim:5.3.2
rm -f gcc-states-latest.osm.pbf gcc-states-latest.osm.pbf.md5 data/riyadh.osm.pbf
```

والمخرج سوف يكون كما يلي:

```text
 Container lab-maps-nominatim Stopping 
 Container lab-maps-nominatim Stopped 
 Container lab-maps-nominatim Removing 
 Container lab-maps-nominatim Removed 
 Volume lab-maps-nominatim-data Removing 
 Network lab-maps-nominatim_default Removing 
 Volume lab-maps-nominatim-data Removed 
 Network lab-maps-nominatim_default Removed 
Untagged: mediagis/nominatim:5.3.2
Deleted: sha256:7923a8e67197fc6d4f4ecb7c0e8bbedffeddcfdf4519596fe946e46a28f5a9f8
```

## تدريب عملي

- استورد مدينتك بدل الرياض، وقارن وقت الاستيراد وحجم قاعدة البيانات وعدد الأماكن التي عليها رقم مبنى بما رأيناه هنا.
- أضف إلى صفحة البحث محاولة ثانية تلقائية تستبدل الهاء في آخر الكلمة بتاء مربوطة إذا لم تأت نتيجة، واختبرها بكلمة «حديقه».

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- البحث عن العناوين يحول النص إلى إحداثيات، والبحث العكسي يعيد أقرب جسم في البيانات لا عنواناً مخترعاً.
- السيرفر العام لـ Nominatim محدود بطلب واحد في الثانية، ويمنع الإكمال التلقائي ويشترط التخزين المؤقت، فإذا كان البحث جزءاً من تطبيقك فاستضفه بنفسك.
- Nominatim يستورد مرة واحدة ويبني العناوين في PostgreSQL مع PostGIS، وفي المختبر أخذت الرياض 6 دقائق و36 ثانية، بأعلى ذاكرة 704 ميغابايت، وقاعدة 432 ميغابايت من ملف 9.6 ميغابايت.
- بالعربية يوحد أشكال الألف، ولكنه لا يعرف الأسماء الدارجة غير الموجودة في البيانات، ولا يساوي بين التاء المربوطة والهاء، ورقم المبنى نادر في بيانات الرياض.
- حدد اللغة بالمعامل `accept-language`، واقرأ الحقل `address` لا `display_name`، وارفض النتائج الأعم مما تحتاج.

[![](https://arabroot.io/content/images/2026/10/self-hosted-maps-cover-cover.webp)دليلخرائطك على سيرفرك مع OpenStreetMap وMapLibreكيف تعرض خريطة تفاعلية لمدينتك من سيرفرك ببيانات OpenStreetMap، بالعربية والإنجليزية ودون مفتاح API أو مزود خارجي.](https://arabroot.io/articles/%D8%AE%D8%B1%D8%A7%D8%A6%D8%B7%D9%83-%D8%B9%D9%84%D9%89-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D9%83-%D9%85%D8%B9-openstreetmap-%D9%88-maplibre/)[![](https://arabroot.io/content/images/2026/10/postgres-for-everything-cover-cover.webp)دليلPostgreSQL لكل شيء: متى يكفيك ومتى تحتاج غيره؟كم عملاً يستطيع PostgreSQL واحد أن يقوم به في شركتك الناشئة قبل أن تحتاج Redis أو Elasticsearch، وما الإشارة التي تقول لك إن الوقت قد حان.](https://arabroot.io/articles/postgresql-%D9%84%D9%83%D9%84-%D8%B4%D9%8A%D8%A1-%D9%85%D8%AA%D9%89-%D8%AA%D9%83%D9%81%D9%8A%D9%83-%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D9%88%D8%A7%D8%AD%D8%AF%D8%A9/)[![](https://arabroot.io/content/images/2026/10/behind-proxy-cover-cover.webp)دليلكيف يرى تطبيقك عنوان الزائر الحقيقي خلف ال Reverse Proxyإذا كان تطبيقك يرى عنوان ال Proxy بدلاً من الزائر أو يولد روابط http، فهذا هو السبب والإعداد الصحيح لإطار عملك.](https://arabroot.io/articles/%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D9%83-%D8%AE%D9%84%D9%81-reverse-proxy/)[![](https://arabroot.io/content/images/2026/10/docker-cover-cover-1.webp)دليلتثبيت Docker Engine وCompose على Ubuntuكيف تثبت Docker على Ubuntu من المستودع الرسمي، وتضبطه من البداية حتى لا يمتلئ القرص أو تنكشف منافذك للإنترنت.](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-docker-%D8%B9%D9%84%D9%89-ubuntu/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ المختبر على mediagis/nominatim:5.3.2 (Nominatim 5.3.2، osm2pgsql 1.11.0، PostgreSQL 16) ببيانات Geofabrik لدول الخليج بتاريخ 2026-10-07 مقصوصة بأداة osmium 1.18.0، وDocker Engine 29.8.0 وDocker Compose v5.5.1 على Windows 11 مع Docker Desktop.