Bỏ qua để đến nội dung

Tìm kiếm & autocomplete

MapsLibVN có hai đường để tìm địa điểm: một web component gắn vào là chạy, và client @mapslibvn/core để tự dựng giao diện. Trang này đi hết cả hai.

Bản UMD tự gọi defineAutocomplete() khi nạp, nên chỉ cần đặt thẻ vào HTML:

<link rel="stylesheet" href="https://mapslibvn-docs.pages.dev/sdk/mapslibvn.css" />
<script src="https://mapslibvn-docs.pages.dev/sdk/mapslibvn.umd.js"></script>
<mapslibvn-autocomplete
id="ac"
api-key="mlv_live_…"
api-base="https://api.ai-solutions.io.vn"
placeholder="Tìm quán cà phê…"
></mapslibvn-autocomplete>
<script>
const map = MapsLibVN.createMap({
container: 'map',
apiKey: 'mlv_live_…',
apiBase: 'https://api.ai-solutions.io.vn',
});
const ac = document.getElementById('ac');
ac.map = map; // dùng cùng Places client/poiSources và lấy tâm bản đồ làm `near`
ac.addEventListener('select', (event) => {
const item = event.detail;
map.flyTo([item.lng, item.lat], 16);
map.addMarker({ lng: item.lng, lat: item.lat, popupHtml: item.name });
});
</script>

Dùng bản ESM thì phải tự đăng ký một lần:

import { defineAutocomplete } from '@mapslibvn/web';
defineAutocomplete();

defineAutocomplete() bỏ qua nếu tên mapslibvn-autocomplete đã được đăng ký, nên gọi nhiều lần vẫn an toàn.

Thuộc tính HTML Bắt buộc Ý nghĩa
api-key Khoá mlv_live_…
api-base Gốc API
placeholder không Mặc định Tìm địa điểm…
near không Chuỗi "lat,lng"vĩ độ trước, khác thứ tự center của bản đồ
sources không Nguồn POI cho chế độ standalone. Khi đã gán .map, component dùng map.places nên tự kế thừa poiSources của map

Đổi api-key hoặc api-base lúc chạy sẽ tạo lại client và huỷ truy vấn đang chờ. Nếu thiếu một trong hai, component thông báo “Thiếu cấu hình API.” và không gọi mạng.

Thuộc tính JS Ý nghĩa
.map Gán đối tượng trả về từ createMap. Component dùng map.places để bản đồ và autocomplete có cùng poiSources; near lấy từ map.gl.getCenter()mỗi lần truy vấn. Cấu hình của map được ưu tiên hơn api-key, api-base, sourcesnear trên element

Đổi hoặc gỡ .map sẽ huỷ kết quả truy vấn đang chờ. Khi .map = null, component quay lại client standalone lấy từ các thuộc tính HTML.

Sự kiện detail
select AutocompleteItem{ type, id?, name, secondary, lat, lng, precision?, score, bbox? }. bubbles: true, composed: true nên bắt được ở phần tử cha

Khi chọn, component tự điền item.name vào ô nhập và đóng danh sách.

  • Ít nhất 2 ký tự sau khi cắt khoảng trắng thì mới gọi API; ngắn hơn thì danh sách được xoá.
  • Debounce 300 ms kể từ lần gõ cuối.
  • Đệm 20 truy vấn gần nhất trong phiên. Gõ thêm một dấu cách (chuỗi gửi đi không đổi) hoặc gõ lùi về chuỗi vừa hỏi xong thì gợi ý hiện ra ngay và không tốn lượt Places. Đệm nằm trong bộ nhớ của trang, mất khi tải lại.
  • Mỗi truy vấn mang số thứ tự riêng; kết quả về muộn của truy vấn cũ bị bỏ qua, không ghi đè kết quả mới.
  • Component gọi autocomplete(q, { near }) với thiết lập mặc định của API: tối đa 10 gợi ý và cả bốn types (poi, street, address, area). Muốn khác thì tự dựng giao diện bằng client ở mục 5, ví dụ types: ['poi', 'street', 'address'] để trở về bộ loại trước khi có area.
  • Mỗi gợi ý có một ký hiệu phân biệt loại ở đầu dòng; vùng hành chính (area) dùng ký hiệu khác POI.
Phím Tác dụng
ArrowDown / ArrowUp Di chuyển trong danh sách, chạy vòng từ cuối về đầu
Enter Chọn mục đang được đánh dấu. Không có mục nào đánh dấu thì không làm gì
Escape Đóng danh sách và huỷ truy vấn đang chờ

Rời ô nhập (blur) thì danh sách đóng sau 150 ms — đủ để cú bấm chuột vào một gợi ý kịp chạy.

Gợi ý type: 'area' mang thêm bbox, nên hãy khớp khung nhìn theo cả vùng thay vì bay tới một điểm. Vùng không có id, vì vậy không gọi getPlace() cho nó.

<mapslibvn-autocomplete id="ac" api-key="mlv_live_…" api-base="https://api.mapslibvn.vn">
</mapslibvn-autocomplete>
<script type="module">
document.getElementById('ac').addEventListener('select', (event) => {
const item = event.detail;
if (item.type === 'area' && item.bbox) map.fitBounds(item.bbox);
else map.flyTo([item.lng, item.lat], 16);
});
</script>

bbox theo thứ tự [minLng, minLat, maxLng, maxLat] — đúng thứ tự MapsLibVNMap.fitBounds() nhận.

Về khả năng tiếp cận: ô nhập là role="combobox" với aria-autocomplete="list", aria-expanded, aria-controlsaria-activedescendant; danh sách là role="listbox" chứa các role="option"aria-selected; một vùng role="status" aria-live="polite" đọc “Đang tìm…”, “Có N kết quả.”, “Không tìm thấy kết quả.” hoặc “Đã chọn …”.

Component dựng trong shadow DOM mở, nên CSS bên ngoài không chạm vào bên trong được. Kích thước và vị trí chỉnh trên chính thẻ <mapslibvn-autocomplete> (thẻ này là display: block).

Nếu đã có bản đồ, dùng luôn map.places — đã mang sẵn khoá và gốc API. Nếu không:

import { createClient } from '@mapslibvn/core';
const client = createClient({
apiKey: 'mlv_live_…',
baseUrl: 'https://api.ai-solutions.io.vn',
});

createClient còn nhận fetch (tiêm bản fetch riêng cho test hoặc môi trường không có global), headers (header thêm cho mọi request, ví dụ X-Bundle-Id cho khoá mobile; không ghi đè được X-Api-Key) và poiSources (mặc định cả hai nguồn) — tập nguồn POI áp cho autocomplete, search, nearby, reversestyleUrl; getPlacegeocode không lọc theo nguồn.

Ba profile là mặc định cả hai (all), ['osm']['fsq']:

createClient({ apiKey, baseUrl, poiSources: ['osm'] });
createClient({ apiKey, baseUrl, poiSources: ['fsq'] });

POI người dùng luôn có trong mọi profile. Nếu archive riêng chưa phát hành, URL style vẫn trả được archive all với x-poi-profile: all;fallback; các Places list route vẫn lọc theo nguồn đã yêu cầu.

const { items } = await client.autocomplete('highlands', {
near: [10.776, 106.7], // [lat, lng]
limit: 5, // 1–10, mặc định 10
types: ['poi', 'address'],
});

Trả { items: AutocompleteItem[] }, đã sắp xếp giảm dần theo score.

Trường Ý nghĩa
type 'poi' một địa điểm, 'street' một tuyến đường, 'address' một mốc số nhà, 'area' một đơn vị hành chính
id Chỉ có với type: 'poi'; truyền vào getPlace(id) để lấy chi tiết. area không có id
name Dòng chính hiển thị
secondary Dòng phụ — với POI là đường, phường, tỉnh ghép lại; với area là tên vùng cũ hoặc danh sách vùng mới
lat, lng Toạ độ
precision Với type: 'address''rooftop'; với type: 'area''province', 'district' hoặc 'ward'
bbox Chỉ có với type: 'area'[minLng, minLat, maxLng, maxLat], truyền thẳng vào map.fitBounds()
matched_alt Chỉ có khi kết quả khớp qua tên thay thế (tên cũ, tên khác trên OSM): tên đó, còn dấu. Gõ cong ly ra “Nam Kỳ Khởi Nghĩa” kèm matched_alt: "Công Lý"
score Điểm xếp hạng tổng hợp (độ giống tên, khoảng cách tới near, độ phổ biến, loại). Xấp xỉ 0–1, chỉ dùng để so sánh trong cùng một lượt gợi ý, không phải xác suất

q phải từ 2 ký tự, nếu không API trả 400 invalid_request. Kết quả được cache 10 phút theo truy vấn đã chuẩn hoá và ô lưới của near, nên hai người gõ giống nhau ở cùng khu vực dùng chung kết quả.

Khớp tên chịu lỗi gõ nhẹ và đảo từ: higland, coffee highlands hay cho ray benh vien vẫn trả đúng địa điểm, vì API so truy vấn với từng đoạn từ của tên chứ không so cả chuỗi. Nhờ vậy một từ nằm giữa tên rất dài cũng tìm được: gõ skincode ra “Showroom Skincode - Swiss Derma Center”. Truy vấn 2–3 ký tự khớp rất nhiều tên, nên thứ tự lúc đó chủ yếu do khoảng cách tới near và độ phổ biến quyết định.

Người Việt viết một địa danh theo nhiều cách, và nhiều tuyến đường vẫn được gọi bằng tên cũ. API xử lý việc này theo ba bậc, và bậc sau chỉ chạy khi bậc trước chưa đủ kết quả — truy vấn thông thường không phải trả giá cho chúng.

Bạn gõ Ra Nhờ đâu
qui nhon Quy Nhơn Từ điển biến thể địa danh, áp thẳng lên truy vấn ở bậc 1
cong ly Nam Kỳ Khởi Nghĩa, kèm matched_alt: "Công Lý" Tên thay thế của tuyến đường lấy từ OSM (bậc 1)
nghia khoi bac Bậc Hai Khởi Nghĩa Bậc 2: mọi từ khớp tiền tố, không kể thứ tự
bin than, kontum Bình Thạnh, Kon Tum Bậc 3: khoá ngữ âm, gộp các cách phát âm và cách viết dính/tách từ

Khoá ngữ âm gộp những khác biệt thường gặp: ch/tr, x/s, d/gi/r, phf, i/y, và âm cuối -ng/-n, -t/-c. Nó gộp được dính/tách từ trong cùng một tên (nha trangnhatrang), nhưng không phải mọi cách viết dính đều gộp.

Điểm của kết quả bậc sau bị trừ một khoảng nhỏ, nên khi bậc 1 đã có kết quả tương đương thì kết quả đó vẫn đứng trước.

const { items, total } = await client.search('bún bò', {
category: 'restaurant',
near: [10.776, 106.7],
radius: 2000, // 1–50 000, mặc định 5 000
limit: 20, // 1–50, mặc định 20
offset: 0, // 0–500
});

Cần ít nhất một trong q, category, near, bbox, nếu không API trả 400 invalid_request. Muốn lọc thuần theo loại thì truyền chuỗi rỗng cho q:

const { items } = await client.search('', { category: 'pharmacy', near: [10.776, 106.7] });

Lọc theo khung nhìn thay vì bán kính:

const b = map.gl.getBounds();
const { items } = await client.search('', {
category: 'cafe',
bbox: [b.getWest(), b.getSouth(), b.getEast(), b.getNorth()],
});

Thứ tự sắp xếp: có q thì theo độ giống tên; không có q nhưng có near thì theo khoảng cách tăng dần; không có cả hai thì theo updated_at mới nhất. total là tổng số bản ghi khớp trước khi cắt theo limit, dùng để phân trang bằng offset.

const { items } = await client.nearby({
lat: 10.776,
lng: 106.7,
radius: 500, // 1–5 000, mặc định 500
category: 'atm',
limit: 20, // 1–100, mặc định 20
});

latlng bắt buộc. Kết quả luôn sắp xếp theo khoảng cách tăng dần. Trả { items }, khôngtotal.

const place = await client.getPlace(poiId);
console.log(place.name, place.address.text, place.hours, place.contact);
console.log(place.sources); // [{ source, source_id, role }]
console.log(place.attribution); // { text, html }

Chỉ POI có statusactive hoặc closed thì ai cũng xem được; POI pending do đóng góp tạo ra chỉ hiện với chính tenant đã tạo. Không tìm thấy thì ném MapsLibVNError với status: 404code: 'not_found'. Phản hồi được cache 1 giờ.

Tình huống Dùng
Ô tìm kiếm gợi ý theo từng ký tự <mapslibvn-autocomplete>, hoặc autocomplete() nếu cần tuỳ biến giao diện
Người dùng gõ xong và bấm “Tìm”, cần danh sách dài, phân trang search()
“Quán cà phê quanh đây”, “ATM gần nhất” nearby()
Đã có id POI (từ poiClick hoặc từ autocomplete) và cần giờ mở cửa, liên hệ, nguồn dữ liệu getPlace()
Có chuỗi địa chỉ đầy đủ, cần toạ độ kèm mức tin cậy geocode() — xem Độ chính xác geocode
Có toạ độ, cần địa chỉ reverse()

Điểm khác nhau đáng nhớ: autocomplete tìm cả POI, đường và mốc số nhà và tối ưu cho độ trễ (cache 10 phút, tối đa 10 kết quả); search chỉ tìm POI nhưng lọc và phân trang được; nearby không nhận từ khoá, chỉ nhận vị trí và loại.

Mọi phương thức ném MapsLibVNError khi API trả mã lỗi:

import { MapsLibVNError } from '@mapslibvn/core';
try {
const { items } = await client.search('bún bò', { near: [10.776, 106.7] });
} catch (err) {
if (err instanceof MapsLibVNError) {
console.error(err.status, err.code, err.message, err.requestId);
if (err.code === 'quota_exceeded') thongBao('Đã hết lượt hôm nay.');
}
}

Các mã hay gặp: invalid_request (400), missing_keyinvalid_key (401), origin_not_allowed (403), not_found (404), quota_exceeded (429), upstream_unavailable (503). Bảng đầy đủ ở REST API.

Các ví dụ trên dùng endpoint nội bộ hiện tại api.ai-solutions.io.vn và SDK phục vụ từ mapslibvn-docs.pages.dev — cả hai là tạm thời trong giai đoạn nội bộ.

Đọc thêm: Bản đồ web, React, REST API, SDK JavaScript.