Navvo
الأدلّة

مكتبة Go (Go SDK)

ثبّت واستخدم عميل Go الرسمي ‎github.com/navvo/navvo-go‎ لخدمات الترميز الجغرافي والمسارات والأماكن وحركة المرور والمركبات الكهربائية والنقل العام والمزيد من جهة الخادم.

مكتبة Go (Go SDK)

وحدة github.com/navvo/navvo-go الرسمية (الحزمة navvo) هي عميل من جهة الخادم بلغة Go لخدمة Navvo على الويب. وهي مكتوبة بأنواع محدّدة، وتعيد المحاولة تلقائيًا، وآمنة للاستخدام المتزامن، وتعتمد على المكتبة القياسية فقط (net/http، encoding/json، context) — بلا أي تبعيات خارجية. يأخذ كل استدعاء وسيطًا أوّلًا من نوع context.Context. لا تحتوي على واجهة خرائط ولا عرض بلاطات — لعرض خريطة في المتصفح استخدم خرائط Navvo.

يتطلّب: Go 1.21+ · عنوان الأساس: https://navvo.io/api/v1

التثبيت

go get github.com/navvo/navvo-go
import navvo "github.com/navvo/navvo-go"

المصادقة

تُرسَل كل الاستدعاءات عبر ترويسة x-api-key. مرّر المفتاح صراحةً، أو مرّر "" لقراءته من متغيّر البيئة NAVVO_API_KEY. يتم التحقق من شكل المفتاح (nvvo_<48 hex>) مسبقًا.

c, err := navvo.New("")                    // يستخدم ‎$NAVVO_API_KEY‎
c, err := navvo.New("nvvo_your_key_here")  // صريح

لا تضع المكتبة المفتاح في عنوان URL إطلاقًا — باستثناء StyleURL() التي تبني عنوان نمط خريطة يجلبه المتصفح، حيث يجب أن يمرّر المفتاح كـ ?api_key= (لا يستطيع المتصفح ضبط ترويسات على طلبات البلاطات/الأنماط).

بداية سريعة

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    navvo "github.com/navvo/navvo-go"
)

func main() {
    c, err := navvo.New("") // يقرأ ‎NAVVO_API_KEY‎
    if err != nil {
        log.Fatal(err)
    }
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    // ترميز جغرافي أمامي — تُحوّل الإحداثيات النصّية إلى أرقام نيابةً عنك.
    hits, err := c.Geocode(ctx, "مستشفى العبدلي، عمّان", &navvo.GeocodeOptions{Limit: 5})
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(hits[0].Lat, hits[0].Lng, hits[0].DisplayName)

    // خطّط مسارًا — تمرّر {Lat, Lng} وتتولّى المكتبة ترتيب lon/lat على الشبكة.
    plan, err := c.Route(ctx, navvo.RoutePlanRequest{
        Locations: []navvo.LatLng{
            {Lat: 31.95, Lng: 35.91, Name: "أ"},
            {Lat: 31.99, Lng: 35.95, Name: "ب"},
        },
        Costing: "auto",
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("الزمن المقدّر (ثوانٍ):", plan.Trip().Summary.Time)

    // فكّ ترميز المسار (polyline6) إلى نقاط.
    line := plan.Geometry()
    fmt.Println("عدد نقاط الشكل:", len(line))

    // إضافات حيّة تراعي الظروف.
    wx, _ := c.WeatherPoint(ctx, 31.95, 35.91, "metric")
    fmt.Println(wx.Temperature, wx.Unit)
}

الإعداد

تقبل New خيارات دالّية:

c, err := navvo.New(
    "",
    navvo.WithBaseURL("https://navvo.io/api/v1"),
    navvo.WithTimeout(15*time.Second),
    navvo.WithUserAgent("acme-dispatch/2.3"),
    navvo.WithRetry(navvo.RetryConfig{
        Retries: 3, BaseDelay: 500 * time.Millisecond, Factor: 2, Jitter: true, MaxDelay: 20 * time.Second,
    }),
)

استخدم navvo.WithHTTPClient(myClient) لتمرير *http.Client خاص بك، و c.WithAPIKey(key) لاستنساخ عميل بمفتاح مختلف (خوادم متعددة المستأجرين). و *Client آمن للاستخدام المتزامن عبر عدّة goroutines.

الإحداثيات — لا تنعكس أبدًا

تمرّر النقاط دائمًا كـ navvo.LatLng{Lat, Lng}. وتتولّى المكتبة كل اختلافات صيغة الشبكة: {lon, lat} لـ Route/TrafficRoute، و{lat, lon} لحمولات المرور المباشرة (SimpleRoute/Matrix/Optimize)، ومعاملا lat/lon المنفصلان للترميز العكسي، وwest,south,east,north لـ BBox، و[lng, lat] لـ GeoJSON، وصيغة WKT POINT(lng lat) لمركز المكان (حلّلها بـ navvo.ParseWKTPoint(place.Center) أو place.CenterLatLng()).

amman := navvo.LatLng{Lat: 31.9539, Lng: 35.9106}
box := navvo.BBox{West: 35.85, South: 31.92, East: 35.98, North: 32.02}

إعادة المحاولة والأخطاء

تُعاد محاولة الأعطال العابرة (5xx، 408، 429/الحصّة، انقطاعات الشبكة، وأظرف available:false) بتراجع أُسّي وعشوَنة كاملة مع احترام Retry-After؛ ولا تُعاد محاولة أخطاء العميل (400/401/403/404) أبدًا. يلفّ كل خطأ *navvo.APIError ويدعم errors.Is / errors.As:

hits, err := c.Geocode(ctx, "مستشفى العبدلي، عمّان", nil)
if err != nil {
    var rl *navvo.RateLimitError
    var scope *navvo.ScopeError
    switch {
    case errors.As(err, &rl):
        log.Printf("تم تجاوز الحدّ، أعد المحاولة بعد %v", rl.RetryAfter)
    case errors.As(err, &scope):
        log.Printf("مشكلة نطاق/حصّة: %s (ينقص %s)", scope.Reason, scope.Scope)
    case errors.Is(err, navvo.ErrServiceUnavailable):
        log.Print("مزوّد الترميز الجغرافي متوقّف — استخدم بديلًا")
    default:
        return err
    }
}

يُفكّ غلاف الترميز/المسارات ({ provider, available, data }) نيابةً عنك — فيصبح جسم available:false خطأً من نوع *navvo.ServiceUnavailableError — ويعيد slug غير معروف للمكان قيمة nil (قيمة فارغة ناعمة، لا خطأً). أَلغِ أو حدّد مهلة أي استدعاء عبر context.Context الذي تمرّره.

الخطأالثابتمن
*ConfigErrorErrConfigمفتاح خاطئ/مفقود أو خيار خاطئ
*ValidationErrorErrValidation400 (يحمل Fields)
*AuthErrorErrUnauthorized401
*ScopeErrorErrForbidden403 (يحمل Reason، Scope)
*NotFoundErrorErrNotFound404
*RateLimitErrorErrRateLimited429 (يحمل RetryAfter)
*ServiceUnavailableErrorErrServiceUnavailableavailable:false، 502/503
*ServerErrorErrServerبقية 5xx
*TimeoutError / *NetworkErrorErrTimeout / ErrNetworkمهلة / نقل

يُرسَل المفتاح فقط في ترويسة x-api-key ولا يظهر في أي خطأ؛ وتحمل الأخطاء RedactedKey (nvvo_****<last4>).

البحث

تُعيد Search نتيجة GET /search بأنواع محدّدة، بما فيها QueryID الذي تشير إليه أحداث النقر اللاحقة. ولا تُرسَل الخيارات المتروكة على قيمتها الصفرية:

res, err := c.Search(ctx, "pharmacy", &navvo.SearchOptions{
    Near:      &navvo.LatLng{Lat: 31.9539, Lng: 35.9106}, // رتّب الأماكن الأقرب أولًا
    RadiusKm:  3,                                         // كلٌّ من RadiusKm و SortDistance يحتاج Near
    Sort:      navvo.SearchSortDistance,
    OpenNow:   navvo.Bool(true),
    MinRating: 4,
    Amenities: []string{"wheelchair-accessible-entrance"},
    Facets:    []navvo.SearchFacet{navvo.SearchFacetCategory, navvo.SearchFacetAmenities},
    Highlight: navvo.Bool(true),
})
if err != nil {
    return err
}
fmt.Println(res.QueryID) // لاستخدامه مع SendSearchEvent
for i, p := range res.Places.Items {
    // تكون DistanceKm و Highlight بقيمة nil حين لا تنطبقان، وتكون ImageURL فارغة حين لا توجد صورة.
    fmt.Println(res.Places.Position(i), p.NameEn, p.DistanceKm, p.ImageURL, p.Promoted)
}
fmt.Println(res.DidYouMean, len(res.Suggestions)) // عند انعدام النتائج فقط: تصحيح إملائي وأماكن شائعة

c.Search(ctx, "rumi caf", &navvo.SearchOptions{Mode: navvo.SearchModeTypeahead})                              // أثناء الكتابة
c.Search(ctx, "quiet place to work with good coffee", &navvo.SearchOptions{Semantic: navvo.SearchSemanticOn}) // المطابقة حسب المعنى
c.Search(ctx, "pharmacy", &navvo.SearchOptions{Analytics: navvo.Bool(false)})                                 // أبقِ هذا الاستدعاء خارج التحليلات
c.SearchSuggestions(ctx, "pha", &navvo.SearchSuggestionsOptions{Limit: 6})                                    // GET /search/suggestions
c.SimilarPlaces(ctx, navvo.SimilarPlacesOptions{Slug: "jordan-museum-994700", Limit: 6})                      // GET /search/similar
  • تبيّن res.QueryUnderstanding() كيف قُرئ الاستعلام (اللغة والنية والتصنيف والمنطقة و«مفتوح الآن»)، وتكون قيمة res.IndexStatus().Mode هي "hybrid" عندما تُسهم المطابقة حسب المعنى. ويحمل res.Raw الجسم كاملًا.
  • مع مفتاح API لا تعمل المطابقة حسب المعنى إلا مع Semantic: navvo.SearchSemanticOn، ولا تعمل أبدًا في نمط الكتابة.
  • تُعيد SearchSuggestions باستعلام فارغ قائمة بداية من التصنيفات اليومية والأماكن المعروفة. وتأخذ SimilarPlaces معرّفًا ID أو Slug وتُعيد Items وSource (SimilarSourceVectors: مقارنة حسب المعنى، أو SimilarSourceNearby: البديل الاحتياطي)، والمكان غير المعروف يُعيد خطأً من النوع *NotFoundError.
  • ترفض Search الاستعلام الفارغ، ونقطة الأصل الواقعة خارج الكرة الأرضية، ومعرّف الجلسة غير الصالح بخطأ من النوع *ValidationError قبل إرسال أي شيء.

تصل الإبرازات مُهرَّبة بترميز HTML، والكلمات المطابقة محاطة بالوسم <em>. وتقسم ParseHighlight الإبراز إلى مقاطع نصية، فتعرضها نصًّا بدل الوثوق بالسلسلة على أنها ترميز:

for _, p := range res.Places.Items {
    if p.Highlight != nil { // nil حين لا يطابق أي اسم، أو حين لم تُطلب الإبرازات
        for _, seg := range navvo.ParseHighlight(p.Highlight.NameEn) {
            fmt.Print(seg.Text, seg.Match)
        }
    }
}

مرشّح السعر وتفاصيل النتيجة

منذ الإصدار v0.3.0 يحتفظ Price بالأماكن التي تقع في أي من مستويات السعر المحدَّدة: 1 حتى 5 دنانير للشخص، و2 من 5 إلى 10، و3 من 10 إلى 20، و4 أكثر من ذلك. ويمكن أيضًا أن يحمل كل SearchItem ما تعرضه قائمة النتائج، وتعني القيمة الصفرية (0 أو "" أو nil) أن بياناته غير متوفّرة للمكان:

res, err := c.Search(ctx, "مقهى", &navvo.SearchOptions{
    Near:  &navvo.LatLng{Lat: 31.9539, Lng: 35.9106},
    Price: []int{1, 2},
})
if err != nil {
    return err
}
for _, p := range res.Places.Items {
    fmt.Println(p.NameEn, p.CategoryName, p.PriceRange, p.PriceLevel, p.ReviewCount, p.Address, p.Area, p.Highlights)
    if p.Hours != nil { // اليوم، بتوقيت الأردن
        fmt.Println(p.Hours.State, p.Hours.ClosesAt, p.Hours.OpensAt, p.Hours.OpensDay)
    }
}
c.SimilarPlaces(ctx, navvo.SimilarPlacesOptions{Slug: "jordan-museum-994700", Locale: "ar"}) // CategoryName و Address بالعربية

تكون قيمة Hours.State إما navvo.SearchHoursOpen أو navvo.SearchHoursClosing (يغلق خلال 45 دقيقة) أو navvo.SearchHoursClosed. ويدلّ Hours.AlwaysOpen على مكان مفتوح على مدار الساعة، وHours.HoursMayVary على عطلة رسمية. ويعيد المستوى الواقع خارج 1–4 خطأً من نوع *ValidationError قبل إرسال أي شيء. راجع تفاصيل النتيجة.

النقرات والتحويلات

لترى نسبة النقر والتحويلات لمشروعك في بوابة المطوّرين، أرسل معرّف جلسة مع البحث، واحدًا لكلّ جلسة مستخدم وفقط إذا وافق مستخدمك على ذلك. ثم أبلِغ عمّا فتحه المستخدم:

session := navvo.NewSearchSessionID() // واحد لكل جلسة مستخدم، وبموافقة المستخدم فقط
res, err := c.Search(ctx, "rumi cafe", &navvo.SearchOptions{Session: session})
if err != nil || len(res.Places.Items) == 0 {
    return err
}
hit, position := res.Places.Items[0], res.Places.Position(0) // النتيجة التي فتحها المستخدم

c.SendSearchEvent(ctx, navvo.SearchEvent{
    QueryID: res.QueryID, Type: navvo.SearchEventClick, PlaceID: hit.ID, Position: position,
})
c.SendSearchEvent(ctx, navvo.SearchEvent{
    QueryID: res.QueryID, Type: navvo.SearchEventConversion, Name: navvo.SearchEventDirections,
    PlaceID: hit.ID, Position: position,
})
  • تُقبل الأحداث خلال 7 أيام من البحث، وفقط إذا حمل البحث معرّف جلسة، وفقط بمفتاح API نفسه.
  • ترسل SendSearchEvents عدّة أحداث معًا، في طلبات لا يتجاوز كلٌّ منها 20 حدثًا و16 كيلوبايت، وتجمع Accepted وRejected وReasons. وأسماء التحويلات هي SearchEventDirections وSearchEventNavigate وSearchEventSave وSearchEventCall وSearchEventWebsite وSearchEventShare.
  • تولّد NewSearchSessionID() دائمًا معرّفًا صالحًا، وتتحقّق ValidSearchSessionID(id) من معرّف: من 16 إلى 64 حرفًا من A-Z a-z 0-9 _ - بلا 6 أرقام متتالية.
  • يُبقي navvo.WithSearchAnalytics(false) كل ما يبحث عنه العميل خارج التحليلات، وتُرفض الأحداث التي يرسلها عميل كهذا، فأرسل النقرات من عميل أُنشئ دون هذا الخيار.

راجع أحداث البحث لمعرفة ما يُقبل ولماذا قد يُرفض حدث، وتحليلات البحث والخصوصية لمعرفة قواعد الموافقة وما تسجّله Navvo.

خريطة الدوال

المجالالدوال
البلاطات والإعدادStyles، StyleURL، MapConfig
البحث والترميز الجغرافيSearch، SearchSuggestions، SimilarPlaces، SendSearchEvent، SendSearchEvents، Geocode، ReverseGeocode، GeocodeBatch
الأماكنListPlaces، Place، PlaceThumb، PlaceCategories
المساراتRoute، SimpleRoute، Matrix، Optimize، RoutingProfiles
حركة المرور والحوادثTrafficRoute، TrafficSummary، Incidents، IncidentTypes، ReportIncident، RoadEdits، RouteRisk
الطقس وجودة الهواءWeatherPoint، WeatherForecast، WeatherCities، AirQualityCities
المركبات الكهربائية والنقلEVStations، TransitStops، GBFS

تقابل كل دالة النقطة الطرفية الموثّقة في مرجع الـ API. تكشف النتائج عن حقل Raw (map[string]any) يحوي الجسم الكامل غير المُنمّط للتوافق المستقبلي. تتطلّب Optimize نطاق optimize الاختياري؛ وتتطلّب TrafficRoute كلًّا من routing وtraffic. راجع تحسين المسارات و حركة المرور والحوادث.

النشر والإصدارات

تُنشَر الوحدة عبر وسوم Git. الإصدار الحالي هو v0.3.0:

go get github.com/navvo/[email protected]
Copyright © 2026