> ## 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.

# خرائطك على سيرفرك مع OpenStreetMap وMapLibre
- URL: https://arabroot.io/articles/خرائطك-على-سيرفرك-مع-openstreetmap-و-maplibre/
- Published: 2026-10-09T10:30:00.000Z
- Updated: 2026-10-09T13:01:18.000Z
- Description: كيف تعرض خريطة تفاعلية لمدينتك من سيرفرك ببيانات OpenStreetMap، بالعربية والإنجليزية ودون مفتاح API أو مزود خارجي.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, مجالات متخصصة, تطبيقات لفريقك, السيادة الرقمية والامتثال, الاستضافة الذاتية

عندما تحتاج خريطة داخل تطبيقك، لصفحة الفروع أو لتتبع المندوبين أو لعرض مواقع العملاء، فالطريقة البسيطة هي أن تنسخ كود Google Maps أو Mapbox، وتضع مفتاح API، وتنتهي المهمة في ربع ساعة. ولكن بعد ذلك سوف تجد أن كل زيارة للخريطة تمر على سيرفر شركة أخرى، وأن الفاتورة تكبر مع عدد الزوار، وأن الشروط والأسعار يمكن أن تتغير في أي وقت، وأن مواقع عملائك ومندوبيك تظهر في سجلات غيرك. وقد يتساءل البعض: لماذا لا أستخدم خرائط OpenStreetMap المجانية مباشرة؟ والإجابة أن البيانات مفتوحة فعلاً، ولكن سيرفرات المربعات Tiles التي تعرضها على موقعهم ليست خدمة مجانية لتطبيقات الآخرين، وسوف نرى شروطها بعد قليل.

والحل الذي نشرحه هنا هو أن تكون الخريطة كلها على سيرفرك: بيانات [OpenStreetMap](https://www.openstreetmap.org/copyright?ref=arabroot.io)، في ملف واحد بصيغة [PMTiles](https://docs.protomaps.com/pmtiles/?ref=arabroot.io)، يقدمه أي سيرفر ويب عادي، وترسمه مكتبة [MapLibre GL JS](https://github.com/maplibre/maplibre-gl-js?ref=arabroot.io) في متصفح الزائر. وهذا هو المقال الأول في مرحلة [الخدمات المتخصصة: الخرائط والبث المباشر ومعالجة الوسائط](https://arabroot.io/start/#stage-special)، وسوف نناقش فيه ما يلي:

- ما هي بيانات OpenStreetMap، وما الذي تطلبه رخصة ODbL منك عند عرضها.
- المربعات النقطية Raster Tiles والمربعات المتجهة Vector Tiles، ولماذا تغير الثانية طريقة استضافة الخرائط.
- صيغة PMTiles وطلبات النطاق HTTP Range Requests التي تجعل ملفاً واحداً يكفي دون سيرفر خرائط.
- مختبر كامل على Docker: خريطة الرياض من سيرفرك بالعربية والإنجليزية، مع المخرجات الحقيقية.
- اختبار صادق للنصوص العربية على الخريطة، ونكسر الإعداد عمداً بسيرفر لا يدعم Range ونقرأ الخطأ.

## بيانات OpenStreetMap ورخصتها

[OpenStreetMap](https://www.openstreetmap.org/copyright?ref=arabroot.io) قاعدة بيانات جغرافية للعالم يحررها متطوعون، فيها الطرق والمباني والأحياء والمحلات وأسماؤها بلغات كثيرة، ومنها الأسماء العربية في `name:ar` والإنجليزية في `name:en`. والمهم هنا أنها بيانات Data وليست خريطة جاهزة، فأنت تأخذ البيانات وتصنع منها خريطتك بالألوان واللغة التي تريدها.

والبيانات مرخصة برخصة [Open Database License](https://opendatacommons.org/licenses/odbl/summary/?ref=arabroot.io) واختصاراً ODbL، وملخصها الرسمي يطلب منك ثلاثة أشياء:

- **نسبة البيانات Attribution:** أي استخدام علني للبيانات أو لعمل ناتج منها يجب أن ينسب إليها، و[إرشادات النسبة](https://osmfoundation.org/wiki/Licence/Attribution%5FGuidelines?ref=arabroot.io) من مؤسسة OpenStreetMap تقول إن النسبة تكون إلى «OpenStreetMap»، ويفضل أن تكون رابطاً إلى صفحة `openstreetmap.org/copyright`، وموضعها التقليدي الزاوية السفلية اليمنى من الخريطة مع قبول أي زاوية.
- **المشاركة بالمثل Share-Alike:** إذا عدلت قاعدة البيانات نفسها ونشرت النسخة المعدلة، أو استخدمتها علناً، فعليك أن تتيح هذه النسخة بالرخصة نفسها.
- **إبقاؤها مفتوحة Keep open:** إذا وزعت البيانات بقيود تقنية فعليك أن توزع معها نسخة دون هذه القيود.

وقد يتساءل البعض: هل يعني ذلك أن مواقع فروعي وعملائي تصبح مفتوحة إذا عرضتها فوق خريطة OpenStreetMap؟ والإجابة في [الأسئلة القانونية الشائعة](https://osmfoundation.org/wiki/Licence/Licence%5Fand%5FLegal%5FFAQ?ref=arabroot.io) للمؤسسة: الخريطة المرسومة تعتبر عملاً ناتجاً Produced Work يمكنك ترخيصه كما تريد، وإذا جمعت بياناتك المستقلة مع بيانات OSM كقواعد منفصلة فهذه قاعدة مجمعة Collective Database وليست قاعدة مشتقة Derivative Database، لذلك لا يلزمك نشر بياناتك، ويبقى عليك شرط النسبة الظاهرة على الخريطة. أما إذا عدلت بيانات OSM نفسها، كأن تصحح الطرق في نسختك وتعرضها، فالنسخة المعدلة تبقى تحت ODbL.

ولماذا لا نأخذ المربعات من سيرفرات OpenStreetMap مباشرة؟ والسبب أن [سياسة استخدام المربعات](https://operations.osmfoundation.org/policies/tiles/?ref=arabroot.io) تقول إن الخدمة على أفضل جهد دون أي اتفاقية مستوى خدمة SLA، وإن الاستخدام الثقيل قد يحجب دون إنذار، وتمنع التحميل الجماعي Bulk Downloading والتحميل المسبق للمربعات التي لا يراها المستخدم، وتنصح من لا تناسبه هذه الشروط باستضافة الخرائط بنفسه أو بمزود آخر، وهذا بالضبط ما سوف نفعله.

## المربعات Tiles: النقطية والمتجهة

الخريطة على الويب لا تحمل كصورة واحدة، وإنما تقسم إلى مربعات Tiles في هرم من مستويات التقريب Zoom: المستوى 0 فيه مربع واحد للعالم كله، والمستوى 1 فيه أربعة، وكل مستوى فيه أربعة أضعاف ما قبله، حتى يصبح في المستوى 15 أكثر من مليار مربع، ولكل مربع عنوان من ثلاثة أرقام `z/x/y`. والمتصفح يطلب فقط المربعات التي تظهر على الشاشة في المستوى الحالي، والصورة التالية تبين الهرم والفرق بين نوعي المربعات:

![مخطط من جزأين: الأول هرم المربعات، المستوى z0 مربع واحد بعنوان 0/0/0، والمستوى z1 أربعة مربعات وأحدها بعنوان 1/1/1، والمستوى z2 ستة عشر مربعاً، والمستوى z15 قرابة مليار، ومستطيل الرياض وحده من المستوى 0 إلى 15 هو 4184 مربعاً في ملف 17 ميغابايت. والثاني مقارنة بين المربع النقطي PNG بالبكسلات المرسومة على السيرفر، باللغة والألوان الثابتة والضبابية عند التقريب، والمربع المتجه MVT بالخطوط والنقاط والأسماء ومنها الاسم العربي، يرسمه المتصفح بملف تنسيق ويحتاج WebGL والخطوط](https://arabroot.io/content/images/2026/10/self-hosted-maps-01-tiles.webp)

كل مستوى تقريب فيه أربعة أضعاف ما قبله، والمربع المتجه يحمل البيانات لا الصورة

- **المربع النقطي Raster Tile:** صورة PNG أو JPEG مرسومة على السيرفر، فاللغة والألوان فيها ثابتة، وتحتاج مجموعة مربعات كاملة لكل لغة ولكل تنسيق، وتبدو ضبابية بين مستويين، ولكنها تعمل في أي عارض ولا تحتاج شيئاً من المتصفح.
- **المربع المتجه Vector Tile:** بيانات مضغوطة بصيغة [Mapbox Vector Tile](https://github.com/mapbox/vector-tile-spec?ref=arabroot.io) واختصاراً MVT، فيها الخطوط والمضلعات والنقاط وأسماؤها بكل اللغات، والمتصفح يرسمها بنفسه بملف تنسيق Style، لذلك تعرض الخريطة نفسها بالعربية أو بالإنجليزية أو بألوان مختلفة من المربعات نفسها، وتبقى حادة عند كل تقريب، ولكنها تحتاج WebGL في المتصفح، وملفات خطوط Fonts، وملف تنسيق.

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

## PMTiles: ملف واحد بدل سيرفر خرائط

الطريقة التقليدية لتقديم المربعات المتجهة هي سيرفر خرائط Tile Server يقرأ من قاعدة SQLite بصيغة MBTiles أو من PostGIS ويجيب على كل طلب `z/x/y`، وهذه خدمة إضافية تشغلها وتراقبها وتحدثها. أما [PMTiles](https://docs.protomaps.com/pmtiles/?ref=arabroot.io) فهي صيغة أرشيف من ملف واحد، والفكرة أن المتصفح يطلب من الملف الأجزاء التي يحتاجها فقط عبر طلبات النطاق HTTP Range Requests، وهي ميزة في HTTP يدعمها أي سيرفر ويب وأي تخزين كائنات Object Storage مثل S3، فلا تحتاج أي برنامج على السيرفر غير سيرفر الملفات. والتوثيق يلخص الفرق: MBTiles مصممة لتقرأ من القرص داخل عملية سيرفر، وPMTiles مصممة لتقرأ عن بعد عبر HTTP مع طلبين وسيطين على الأكثر يمكن تخزينهما مؤقتاً Cache. والصورة التالية تبين ما يحدث عند فتح الخريطة:

![مخطط: المتصفح بمكتبة MapLibre وبروتوكول pmtiles.js يطلب من nginx أول 16384 بايت من riyadh.pmtiles فيرد 206 Partial Content بالترويسة والدليل الأول، ثم يطلب بايتات المربعات الظاهرة فقط فترد 206 بأحجام من 16 إلى 99 كيلوبايت، وأول شاشة في المختبر احتاجت 13 طلباً و652 كيلوبايت كلها من 127.0.0.1. وتحته ترتيب الملف: ترويسة 127 بايت، ثم دليل أول داخل أول 16 كيلوبايت، ثم البيانات الوصفية، ثم أدلة فرعية، ثم بيانات 4123 مربعاً مضغوطة](https://arabroot.io/content/images/2026/10/self-hosted-maps-02-pmtiles.webp)

المتصفح يقرأ الدليل أولاً، ثم يطلب البايتات التي فيها المربعات الظاهرة فقط

و[مواصفة PMTiles v3](https://github.com/protomaps/PMTiles/blob/main/spec/v3/spec.md?ref=arabroot.io) تحدد أن الملف يبدأ بترويسة Header طولها 127 بايت، وأن الدليل الأول Root Directory يجب أن يكون داخل أول 16384 بايت، حتى يجلبهما الكلاينت في طلب واحد، ثم الأدلة تحول عنوان كل مربع `z/x/y` إلى موضع وطول داخل الملف. ومصدر الملف عندنا هو [الخريطة الأساسية من Protomaps](https://docs.protomaps.com/basemaps/downloads?ref=arabroot.io)، وهي بناء يومي للعالم كله من بيانات OpenStreetMap من المستوى 0 إلى 15، وصفحة التحميل تقول إنها موزعة كعمل ناتج Produced Work تحت ODbL مع اشتراط نسبة OpenStreetMap، وإن الربط المباشر Hotlinking بملفات التحميل غير مستحب، وإن عليك نسخ الملف إلى تخزينك. والكود والتنسيق في [مستودع basemaps](https://github.com/protomaps/basemaps?ref=arabroot.io) برخصة BSD-3 والتصميم البصري CC0.

## MapLibre GL JS: مكتبة الرسم في المتصفح

[MapLibre GL JS](https://github.com/maplibre/maplibre-gl-js?ref=arabroot.io) مكتبة مفتوحة المصدر بترخيص [BSD-3-Clause](https://github.com/maplibre/maplibre-gl-js/blob/main/LICENSE.txt?ref=arabroot.io) ترسم المربعات المتجهة بال WebGL، وكما يذكر ملف المشروع فقد بدأت نسخة مفتوحة من mapbox-gl-js قبل أن تنتقل Mapbox إلى رخصة غير مفتوحة في ديسمبر 2020\. وفي الطبقة نفسها نستخدم ثلاث مكتبات صغيرة: [pmtiles](https://github.com/protomaps/PMTiles?ref=arabroot.io) التي تعلم MapLibre قراءة روابط `pmtiles://` بطلبات النطاق، و[@protomaps/basemaps](https://docs.protomaps.com/basemaps/maplibre?ref=arabroot.io) التي تبني طبقات التنسيق Layers بلغة تختارها، وملفات الخطوط والرموز Sprites من مستودع [basemaps-assets](https://github.com/protomaps/basemaps-assets?ref=arabroot.io)، والخطوط فيه برخصة SIL Open Font License والرموز مشتقة من أيقونات برخصة MIT.

## التجربة العملية: خريطة الرياض من سيرفرك

سوف نقص مستطيل مدينة الرياض من آخر بناء يومي لخريطة Protomaps، ونقدمه مع ملفات MapLibre والخطوط من nginx على 127.0.0.1 فقط، ثم نفتح الخريطة في المتصفح ونتحقق أن كل الطلبات خرجت إلى سيرفرنا وحده. والصورة التالية تبين المسار كاملاً ومن يرخص كل جزء:

![مخطط من خمس خطوات: بيانات OpenStreetMap برخصة ODbL 1.0، ثم البناء اليومي من Protomaps للعالم من المستوى 0 إلى 15 بحجم 139 جيجابايت، بيانات ODbL وكود BSD-3، ثم الأمر pmtiles extract بمستطيل الرياض في 15 ثانية و43 طلباً وملف 17 ميغابايت، ثم سيرفرك nginx فيه ملف المربعات وMapLibre وpmtiles.js والتنسيق والخطوط OFL والرموز MIT دون سيرفر خرائط ودون قاعدة بيانات ودون مفتاح API، ثم متصفح الزائر يرسم الخريطة بالعربية أو الإنجليزية مع نسبة OpenStreetMap على الخريطة](https://arabroot.io/content/images/2026/10/self-hosted-maps-03-stack.webp)

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

### قص المنطقة التي تحتاجها

نبدأ بأداة [go-pmtiles](https://github.com/protomaps/go-pmtiles?ref=arabroot.io) من Docker Image المثبتة على الإصدار `v1.31.2`، ونقرأ وصف آخر بناء يومي دون تحميله:

```bash
mkdir -p maps-lab/web/tiles && cd maps-lab
docker run --rm --name lab-maps-pmtiles protomaps/go-pmtiles:v1.31.2 \
  show https://build.protomaps.com/20261008.pmtiles
```

والمخرج سوف يكون كما يلي (اختصرنا بعض الأسطر):

```text
pmtiles spec version: 3
tile type: mvt
bounds: (long: -180.000000, lat: -85.051129) (long: 180.000000, lat: 85.051129)
min zoom: 0
max zoom: 15
addressed tiles count: 1431655765
tile entries count: 178156644
tile contents count: 136266273
clustered: true
tile compression: gzip
planetiler:osm:osmosisreplicationtime 2026-10-08T04:00:00Z
planetiler:version 0.10.2
version 4.15.2
attribution <a href="https://www.openstreetmap.org/copyright" target="_blank">&copy; OpenStreetMap</a>
description Basemap layers derived from OpenStreetMap and Natural Earth
name Protomaps Basemap
```

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

- الأمر قرأ الترويسة والبيانات الوصفية من ملف على الإنترنت حجمه 138,703,391,160 بايت، أي قرابة 139 جيجابايت بحسب [قائمة البناءات](https://build-metadata.protomaps.dev/builds.json?ref=arabroot.io)، دون أن يحمله، وهذه هي طلبات النطاق نفسها التي سوف يستخدمها المتصفح.
- البيانات من OpenStreetMap حتى الساعة الرابعة من صباح اليوم نفسه، وبنيت بأداة [Planetiler](https://github.com/onthegomap/planetiler?ref=arabroot.io)، والنسبة المطلوبة مكتوبة داخل الملف في `attribution`.

الآن نقص مستطيل الرياض بالإحداثيات بالترتيب: خط الطول الغربي، ثم خط العرض الجنوبي، ثم الشرقي، ثم الشمالي:

```bash
docker run --rm --name lab-maps-pmtiles -v "$PWD/web/tiles:/data" protomaps/go-pmtiles:v1.31.2 \
  extract https://build.protomaps.com/20261008.pmtiles /data/riyadh.pmtiles \
  --bbox=46.45,24.45,47.05,25.00
```

والمخرج سوف يكون كما يلي (حذفنا أسطر شريط التقدم):

```text
2026/10/08 19:58:02 extract.go:401: fetching 9 dirs, 9 chunks, 8 requests
2026/10/08 19:58:07 extract.go:441: Region tiles 4249, result tile entries 4184
2026/10/08 19:58:07 extract.go:450: fetching 4184 tiles, 94 chunks, 32 requests
2026/10/08 19:58:15 extract.go:606: Completed in 15.25124163s with 4 download threads (274.3383177626678 tiles/s).
2026/10/08 19:58:15 extract.go:611: Extract required 43 total requests.
2026/10/08 19:58:15 extract.go:612: Extract transferred 18 MB (overfetch 0.05) for an archive size of 17 MB
```

أي أن مدينة الرياض كاملة من المستوى 0 إلى 15 أصبحت ملفاً واحداً حجمه 16,989,977 بايت، أي قرابة 17 ميغابايت، في 15 ثانية و43 طلباً فقط. ونتأكد من الملف بأمرين:

```bash
docker run --rm --name lab-maps-pmtiles -v "$PWD/web/tiles:/data" protomaps/go-pmtiles:v1.31.2 show /data/riyadh.pmtiles
docker run --rm --name lab-maps-pmtiles -v "$PWD/web/tiles:/data" protomaps/go-pmtiles:v1.31.2 verify /data/riyadh.pmtiles
```

والمخرج سوف يكون كما يلي (من الأمر الأول أول اثني عشر سطراً، ومن الثاني السطر الأخير):

```text
pmtiles spec version: 3
tile type: mvt
bounds: (long: 46.450000, lat: 24.450000) (long: 47.050000, lat: 25.000000)
min zoom: 0
max zoom: 15
center: (long: 46.750000, lat: 24.725000)
center zoom: 0
addressed tiles count: 4249
tile entries count: 4184
tile contents count: 4123
clustered: true
internal compression: gzip
2026/10/08 20:04:53 verify.go:169: Completed verify in 16.505271ms.
```

لاحظ أن عدد المربعات المختلفة 4123 وهو أقل من 4184، والسبب أن المربعات المتطابقة، مثل مربعات الصحراء الفارغة، تحفظ مرة واحدة ويشير إليها أكثر من عنوان.

### ملفات الموقع

بعد ذلك نضع ملفات المكتبات بإصدارات مثبتة داخل مجلد `web`، حتى لا يطلب المتصفح أي شيء من خارج سيرفرك:

```bash
cd web && mkdir -p lib assets && cd lib
for f in maplibre-gl.mjs maplibre-gl-worker.mjs maplibre-gl.css; do
  curl -sfLO https://unpkg.com/maplibre-gl@6.13.0/dist/$f
done
curl -sfL -o pmtiles.mjs  https://unpkg.com/pmtiles@4.5.0/dist/esm/index.js
curl -sfL -o fflate.mjs   https://unpkg.com/fflate@0.8.2/esm/browser.js
curl -sfL -o basemaps.mjs https://unpkg.com/@protomaps/basemaps@5.7.2/dist/esm/index.js
cd ../assets
curl -sfL -o assets.zip https://github.com/protomaps/basemaps-assets/archive/refs/heads/main.zip
unzip -q assets.zip && mv basemaps-assets-main/fonts basemaps-assets-main/sprites . && rm -rf basemaps-assets-main assets.zip
cd ../.. && du -sh web/*
```

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

```text
19M	web/assets
1.8M	web/lib
17M	web/tiles
```

ثم صفحة الخريطة `web/index.html`:

```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%; }</style>
<script type="importmap">{ "imports": { "fflate": "/lib/fflate.mjs" } }</script>
</head>
<body>
<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';

// let MapLibre read pmtiles:// URLs with HTTP range requests
const protocol = new Protocol();
maplibregl.addProtocol('pmtiles', protocol.tile);

const lang = new URLSearchParams(location.search).get('lang') || 'ar';
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 })
  }
});
map.addControl(new maplibregl.NavigationControl());
map.on('error', (e) => { window.__err = String(e.error && e.error.message); console.error(e.error); });
</script>
</body>
</html>
```

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

- MapLibre 6 تأتي كوحدات ES Modules، ومكتبة pmtiles بصيغة ESM تستورد مكتبة الضغط `fflate` باسمها، لذلك وضعنا Import Map يحول هذا الاسم إلى الملف المحلي.
- `addProtocol('pmtiles', …)` هو السطر الذي يجعل الرابط `pmtiles:///tiles/riyadh.pmtiles` يقرأ بطلبات النطاق من الملف نفسه.
- الدالة `layers` من basemaps تبني طبقات التنسيق، والخيار `lang` يحدد لغة الأسماء، فنغيره من الرابط `?lang=en` دون أن نغير المربعات.
- الخطوط Glyphs والرموز Sprites من مجلد `assets` على السيرفر نفسه، و`attribution` هي النسبة التي تظهر في زاوية الخريطة كما تطلب الرخصة.

ثم إعداد nginx في `nginx.conf`:

```nginx
server {
    listen 80;
    root /usr/share/nginx/html;
    types {
        text/html html;
        text/css css;
        text/javascript js mjs;
        application/json json;
        image/png png;
        application/x-protobuf pbf;
        application/vnd.pmtiles pmtiles;
    }
    location / {
        try_files $uri $uri/ =404;
    }
}
```

وحددنا أنواع الملفات MIME Types بأنفسنا، لأن المتصفح يرفض تشغيل وحدة JavaScript من ملف `.mjs` إذا لم يصل بنوع JavaScript. أما طلبات النطاق فلا تحتاج أي سطر، لأن nginx يدعمها للملفات الثابتة كما سوف نرى في السجل. وأخيراً ملف `compose.yaml`:

```yaml
name: lab-maps
services:
  web:
    image: nginx:1.29-alpine
    container_name: lab-maps-web
    volumes:
      - ./web:/usr/share/nginx/html:ro
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    ports:
      - "127.0.0.1:18701:80"
```

### تشغيل الخريطة

```bash
docker compose up -d
curl -s -r 0-126 -D - -o /dev/null http://127.0.0.1:18701/tiles/riyadh.pmtiles
```

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

```text
 Container lab-maps-web Created
 Container lab-maps-web Starting
 Container lab-maps-web Started
HTTP/1.1 206 Partial Content
Server: nginx/1.29.8
Date: Thu, 08 Oct 2026 20:00:27 GMT
Content-Type: application/vnd.pmtiles
Content-Length: 127
Last-Modified: Thu, 08 Oct 2026 19:58:15 GMT
Connection: keep-alive
ETag: "6ac7f5d7-1033f19"
Content-Range: bytes 0-126/16989977
```

طلبنا الترويسة فقط، أي البايتات من 0 إلى 126، فرد nginx بالحالة `206 Partial Content` وأرسل 127 بايتاً من ملف حجمه 16,989,977 بايت، وهذا هو كل ما تحتاجه PMTiles من السيرفر. الآن افتح `http://127.0.0.1:18701` في المتصفح، والصورة التالية هي الخريطة كما ظهرت:

![خريطة الرياض بالمستوى 12 بأسماء عربية متصلة الحروف: طريق الملك فهد وطريق الملك عبدالله والدائري الشمالي وحديقة الملك سلمان بالأخضر وأحياء العليا والسليمانية والملز والدرعية، وفي الزاوية السفلية MapLibre و© OpenStreetMap، وبعض الأسماء بالإنجليزية مثل As Sil Kbeer وAl Taief](https://arabroot.io/content/images/2026/10/self-hosted-maps-04-riyadh-ar.webp)

خريطة الرياض من سيرفرنا بالعربية، والنسبة © OpenStreetMap في الزاوية

ولنعرف من أين جاءت الخريطة فعلاً، نقرأ قائمة الطلبات Resource Timing في المتصفح بعد اكتمال الرسم، بلصق السطر التالي في أدوات المطور Console:

```javascript
JSON.stringify((() => { const r = performance.getEntriesByType('resource'); const hosts = [...new Set(r.map(e => new URL(e.name).host))]; const t = r.filter(e => e.name.includes('.pmtiles')); return { hosts, requests: r.length, pmtilesRequests: t.length, pmtilesBytes: t.reduce((a, e) => a + e.transferSize, 0), zoom: map.getZoom() }; })())
```

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

```text
{"hosts":["127.0.0.1:18701"],"requests":28,"pmtilesRequests":13,"pmtilesBytes":651775,"zoom":12}
```

أي أن الشاشة الأولى احتاجت 28 طلباً كلها إلى سيرفرنا، منها 13 طلب نطاق على ملف المربعات بمجموع 651,775 بايت، ولم يخرج أي طلب إلى Google أو Mapbox أو حتى إلى OpenStreetMap. وفي سجل nginx سوف تجد هذه الطلبات بالحالة 206:

```bash
docker logs lab-maps-web 2>&1 | grep "riyadh.pmtiles" | awk '{print $9, $10}' | head -6
```

```text
206 127
206 16384
206 57529
206 69931
206 68549
206 99121
```

السطر الأول هو طلب curl السابق، والثاني هو أول طلب من المتصفح: 16384 بايت فيها الترويسة والدليل الأول، كما تنص المواصفة، ثم طلبات المربعات الظاهرة. ونفس الخريطة بالإنجليزية من الرابط `?lang=en`، دون أي تغيير في المربعات:

![الخريطة نفسها بالإنجليزية: King Salman Park وKing Khalid Road وAl Wurud وAl Muhammadiyah وAs Sulaymaniyah، وتحت كل اسم إنجليزي الاسم العربي بخط أصغر](https://arabroot.io/content/images/2026/10/self-hosted-maps-05-riyadh-en.webp)

المربعات نفسها بالإنجليزية، والتنسيق يضع الاسم المحلي تحت الاسم الإنجليزي

## النصوص العربية على الخريطة: اختبار صادق

النص العربي على الخرائط في المتصفح كان مشكلة قديمة، لأن الحروف يجب أن تتصل وتعكس اتجاهها، ومكتبات الخرائط كانت ترسم كل حرف وحده. وفي [سجل تغييرات MapLibre](https://github.com/maplibre/maplibre-gl-js/blob/main/CHANGELOG.md?ref=arabroot.io) للإصدار 6.9.0 أن المكتبة أصبحت ترسم العربية والعبرية بشكل صحيح دون إضافة النصوص من اليمين إلى اليسار RTL Text Plugin، وأن الدالة `setRTLTextPlugin` أصبحت مهملة Deprecated. ولم نكتف بما في السجل، فشغلنا الصفحة نفسها بالإصدار الأقدم 5.24.0 مرة دون الإضافة ومرة مع [mapbox-gl-rtl-text](https://github.com/mapbox/mapbox-gl-rtl-text?ref=arabroot.io) الإصدار 0.3.0، والصورة التالية تبين الفرق:

![صورتان للرياض بالإصدار 5.24.0 من MapLibre: على اليمين دون الإضافة، والأسماء العربية حروفها منفصلة ومعكوسة الترتيب ولا تقرأ، وعلى اليسار مع إضافة mapbox-gl-rtl-text، والأسماء متصلة وصحيحة مثل طريق الملك فهد وحديقة الملك سلمان](https://arabroot.io/content/images/2026/10/self-hosted-maps-06-maplibre5-arabic.webp)

MapLibre 5.24.0: يمين الصورة دون إضافة RTL والحروف منفصلة ومعكوسة، ويسارها مع الإضافة

ومن هذا الاختبار ثلاث نتائج عملية:

- إذا كان تطبيقك على MapLibre من الإصدار 6.9.0 فما بعده فالعربية تعمل دون أي سطر إضافي، كما في صورة الرياض أعلاه بالإصدار 6.13.0.
- إذا كنت على إصدار أقدم ولا تستطيع الترقية الآن، فأضف `setRTLTextPlugin` قبل إنشاء الخريطة، وإلا فسوف يرى المستخدم العربي ما في يمين الصورة.
- الخريطة تعرض `name:ar` إذا كان موجوداً في OpenStreetMap، وإلا فالاسم الأصلي `name`، لذلك سوف ترى بعض الأماكن بالإنجليزية مثل As Sil Kbeer وAl Taief في صورة الرياض، وبعض المطاعم بأسمائها الإنجليزية في الصورة التالية، والحل ليس في الكود وإنما في البيانات: أضف الاسم العربي في OpenStreetMap نفسه فيصل إلى كل من يستخدمها في البناء التالي.

![الخريطة بالمستوى 16 قرب شارع العليا: أسماء الشوارع العربية مثل رقم 92 ورقم 90 ووادي حنيفة وشارع موسى بن نصير، وأيقونات مطاعم بعضها بالعربية مثل مطعم فول وعدس وسالدوتش وبعضها بالإنجليزية مثل Kudu وThe Pancake House وRoma Restaurant](https://arabroot.io/content/images/2026/10/self-hosted-maps-07-olaya-z16.webp)

المستوى 16 قرب العليا: المطاعم التي ليس لها اسم عربي في البيانات تظهر بالإنجليزية

ولاحظ أن البناء من Protomaps يتوقف عند المستوى 15، وما بعده تكبير للمربعات نفسها Overzoom يرسمه المتصفح بحدة لأنها متجهة، ففي المستوى 16 احتاجت الشاشة 25 طلب نطاق بمجموع 974,729 بايت، وكلها أيضاً من `127.0.0.1:18701`.

## نكسر الإعداد: سيرفر لا يدعم Range

ماذا يحدث إذا وضعت الملفات على سيرفر لا يدعم طلبات النطاق؟ لنأخذ مثالاً سيرفر Python المدمج، فنقدم المجلد نفسه على منفذ آخر:

```bash
docker run -d --name lab-maps-norange -v "$PWD/web:/srv:ro" -w /srv -p 127.0.0.1:18702:8000 \
  python:3.12-slim python -m http.server 8000
curl -s -r 0-126 -D - -o /dev/null http://127.0.0.1:18702/tiles/riyadh.pmtiles | head -5
```

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

```text
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.12.15
Date: Thu, 08 Oct 2026 20:05:05 GMT
Content-type: application/octet-stream
Content-Length: 16989977
```

طلبنا 127 بايتاً فرد السيرفر بالحالة 200 وأرسل الملف كاملاً، وعند فتح `http://127.0.0.1:18702` تبقى الخريطة فارغة، والخطأ الذي التقطناه من حدث `error` في MapLibre هو:

```text
Server returned no content-length header or content-length exceeding request. Check that your storage backend supports HTTP Byte Serving.
```

وهذه الرسالة تعني أن السيرفر أو شيئاً أمامه، مثل Reverse Proxy أو CDN أو إعداد ضغط خاطئ، يتجاهل الترويسة `Range`، فإذا رأيتها فابدأ بالأمر `curl -r 0-126 -D -` كما فعلنا، وابحث عن `206` و`Content-Range` في الرد. ثم نحذف هذا السيرفر بالاسم:

```bash
docker rm -f lab-maps-norange
```

## النسبة على خريطتك عملياً

- اترك عنصر النسبة Attribution Control ظاهراً في زاوية الخريطة، وMapLibre يعرضه تلقائياً من الحقل `attribution` في المصدر، كما في الزاوية السفلية في الصور أعلاه.
- [إرشادات النسبة](https://osmfoundation.org/wiki/Licence/Attribution%5FGuidelines?ref=arabroot.io) تسمح بطيها Collapse بشروط، مثل أن تطوى عند تحريك الخريطة أو بعد خمس ثوان، بشرط أن يجد المستخدم معلومات الرخصة إذا بحث عنها، مثل زر (i) في زاوية الخريطة، وهو ما يفعله MapLibre في الشاشات الصغيرة.
- إذا نشرت لقطة شاشة من خريطتك في تقرير أو عرض، فهي أيضاً عمل ناتج من البيانات، فاترك النسبة ظاهرة فيها.

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

- **الدومين و HTTPS:** ضع nginx خلف Reverse Proxy بشهادة TLS كما في [تطبيقك خلف Reverse Proxy](https://arabroot.io/articles/%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D9%83-%D8%AE%D9%84%D9%81-reverse-proxy/)، وتأكد بعد ذلك بالأمر `curl -r` أن ال Proxy يمرر طلبات النطاق ويرد بالحالة 206.
- **التخزين المؤقت Cache:** ملف المربعات لا يتغير إلا عندما تستبدله، لذلك يمكن أن تضع له مدة تخزين طويلة في المتصفح أو في [شبكة توصيل المحتوى CDN](https://arabroot.io/articles/%D8%B4%D8%A8%D9%83%D8%A9-%D8%AA%D9%88%D8%B5%D9%8A%D9%84-%D8%A7%D9%84%D9%85%D8%AD%D8%AA%D9%88%D9%89-cdn-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، وعند التحديث غير اسم الملف، مثل `riyadh-20261008.pmtiles`، حتى لا يخلط المتصفح بين نسختين.
- **التخزين:** يمكن أن تضع الملف في تخزين كائنات Object Storage متوافق مع S3 بدل القرص، كما شرحنا في [أنواع التخزين](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%A7%D9%84%D8%AA%D8%AE%D8%B2%D9%8A%D9%86-block-%D9%88-file-%D9%88-object-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، بشرط أن يدعم طلبات النطاق وأن تضبط CORS إذا كان على دومين آخر.
- **التحديث:** البيانات في المختبر من يوم القص، والبناء يومي، فحدد كم مرة تحتاج تحديث خريطتك، وأعد أمر `pmtiles extract` بالبناء الأحدث، وثبت تاريخ البناء في الأمر كما فعلنا بدل أن تعتمد على رابط يتغير.
- **المنطقة:** كلما اتسع المستطيل كبر الملف، فاقطع المنطقة التي يحتاجها تطبيقك فقط، ويمكن أن تقطع منطقة أوسع بمستويات أقل بالخيار `--maxzoom`.

## ماذا بعد الخريطة؟

الخريطة تعرض الأماكن، ولكن تطبيقك غالباً يحتاج أن يحول عنواناً يكتبه المستخدم إلى نقطة على الخريطة، أو نقطة من GPS الهاتف إلى عنوان، وهذا ما نشرحه في المقال التالي [البحث عن العناوين Geocoding مع Nominatim](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D8%AD%D8%AB-%D8%B9%D9%86-%D8%A7%D9%84%D8%B9%D9%86%D8%A7%D9%88%D9%8A%D9%86-geocoding-%D9%85%D8%B9-nominatim/) على مستطيل الرياض نفسه. وحساب المسارات والمسافات، وتحليل البيانات الجغرافية في PostGIS، موضوعات مخططة في [المرحلة نفسها](https://arabroot.io/start/#stage-special)، وإذا كانت بياناتك الجغرافية في PostgreSQL أصلاً فاقرأ قسم [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).

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

من داخل مجلد المختبر نحذف ال Container والشبكة بالاسم:

```bash
docker compose down
```

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

```text
 Container lab-maps-web Stopping 
 Container lab-maps-web Stopped 
 Container lab-maps-web Removing 
 Container lab-maps-web Removed 
 Network lab-maps_default Removing 
 Network lab-maps_default Removed
```

ثم نحذف Docker Image الخاصة بالأداة إذا لم تعد تحتاجها، ومجلد المختبر الذي فيه ملف المربعات:

```bash
docker image rm protomaps/go-pmtiles:v1.31.2
cd .. && rm -rf maps-lab
```

## تدريب عملي

- اقطع مستطيل مدينتك بدل الرياض، وقارن حجم الملف وعدد المربعات بما رأيناه هنا، ثم اقطع المنطقة نفسها بالخيار `--maxzoom=12` وقارن الحجم مرة أخرى.
- غير `namedFlavor('light')` إلى `dark` وغير مسار الرموز إلى `sprites/v4/dark`، وأضف علامة Marker لموقع فرعك بالدالة `new maplibregl.Marker()`.

## الخلاصة

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

- بيانات OpenStreetMap مفتوحة برخصة ODbL، والخريطة التي ترسمها منها عمل ناتج ترخصه كما تريد، بشرط نسبة «OpenStreetMap» ظاهرة على الخريطة، أما تعديل البيانات نفسها ونشرها فيبقى تحت ODbL.
- سيرفرات مربعات OpenStreetMap ليست لخدمة تطبيقك، فاستضف خريطتك بنفسك.
- المربعات المتجهة تنقل الرسم إلى المتصفح، فتعرض العربية والإنجليزية من الملف نفسه، وPMTiles تجعل المربعات ملفاً واحداً يقدمه أي سيرفر يدعم طلبات النطاق.
- في المختبر أصبحت الرياض من المستوى 0 إلى 15 ملفاً من 17 ميغابايت في 15 ثانية، والشاشة الأولى احتاجت 13 طلب نطاق بقرابة 652 كيلوبايت، وكل الطلبات إلى سيرفرنا.
- MapLibre من الإصدار 6.9.0 ترسم العربية صحيحة دون إضافة، والإصدارات الأقدم تحتاج `setRTLTextPlugin`، والأسماء الناقصة بالعربية تصلح في بيانات OpenStreetMap لا في الكود.
- إذا رأيت الخطأ HTTP Byte Serving فالسيرفر أو ما أمامه يتجاهل Range، وتأكد دائماً بالأمر `curl -r 0-126 -D -`.

[![](https://arabroot.io/content/images/2026/10/nominatim-cover-cover.webp)دليلالبحث عن العناوين Geocoding مع Nominatimكيف تحول اسم المكان إلى إحداثيات والإحداثيات إلى عنوان على سيرفرك مع Nominatim، وما الذي يجيده بالعربية وما الذي يفوته.](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D8%AD%D8%AB-%D8%B9%D9%86-%D8%A7%D9%84%D8%B9%D9%86%D8%A7%D9%88%D9%8A%D9%86-geocoding-%D9%85%D8%B9-nominatim/)[![](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/cdn-basics-cover-cover.webp)دليلكيف تعمل شبكة توصيل المحتوى CDN وما الذي تحفظهلماذا يبطؤ موقعك على الزائر البعيد، وكيف تقرب شبكة CDN ملفاتك منه، وكيف تمنعها من حفظ الصفحات الخاصة.](https://arabroot.io/articles/%D8%B4%D8%A8%D9%83%D8%A9-%D8%AA%D9%88%D8%B5%D9%8A%D9%84-%D8%A7%D9%84%D9%85%D8%AD%D8%AA%D9%88%D9%89-cdn-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/storage-types-cover-cover.webp)دليلأنواع التخزين وواجهة S3: أين تضع بيانات تطبيقك؟أين تضع قاعدة البيانات وملفات المستخدمين والنسخ الاحتياطية، ولماذا يناسب كلاً منها نوع مختلف من التخزين.](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%A7%D9%84%D8%AA%D8%AE%D8%B2%D9%8A%D9%86-block-%D9%88-file-%D9%88-object-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/open-source-licenses-cover-cover.webp)دليلما هي البرمجيات مفتوحة المصدر؟ القصة وأشهر الرخصكيف بدأت البرمجيات الحرة والمفتوحة، وما الفرق بين الحر والمجاني، وكيف تعرف ما تسمح به رخصة أي مشروع قبل أن تبني عليه.](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%8A-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D8%A7%D8%AA-%D9%85%D9%81%D8%AA%D9%88%D8%AD%D8%A9-%D8%A7%D9%84%D9%85%D8%B5%D8%AF%D8%B1-%D9%88%D8%AA%D8%B1%D8%A7%D8%AE%D9%8A%D8%B5%D9%87%D8%A7/)

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

- أكتوبر 2026: كتابة المقال، وتنفيذ المختبر على بناء Protomaps بتاريخ 20261008 (الإصدار 4.15.2)، وgo-pmtiles 1.31.2، وMapLibre GL JS 6.13.0 و5.24.0 للمقارنة، وpmtiles 4.5.0، و@protomaps/basemaps 5.7.2، وnginx 1.29.8، وDocker Engine 29.8.0 وDocker Compose v5.5.1 على Windows 11 مع Docker Desktop.