mapbox-search-integration by mapbox/mapbox-agent-skills
npx skills add https://github.com/mapbox/mapbox-agent-skills --skill mapbox-search-integration在应用程序中实现 Mapbox 搜索功能的专家指南。涵盖从提出正确的发现性问题、选择适当的搜索产品,到遵循 Mapbox 搜索团队的最佳实践实现生产就绪的集成的完整工作流程。
当用户说类似以下的话时:
此技能是对 mapbox-search-patterns 的补充:
mapbox-search-patterns = 工具和参数选择mapbox-search-integration = 完整的实现工作流程在开始编码之前,先问这些问题以了解需求:
提问: "您希望用户搜索什么?"
常见答案及其含义:
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
如果最初未说明,请跟进提问: "您的用户是否需要搜索兴趣点数据?比如餐厅、商店、商业类别?"
含义:
提问: "用户将在哪里进行搜索?"
常见答案及其含义:
country 参数,结果更好,成本更低bbox 参数进行边界框约束country 数组参数跟进提问: "您是否需要将结果限制在特定区域?"(配送区、服务区等)
提问: "用户将如何与搜索交互?"
常见答案及其含义:
auto_complete: true,或对于 Geocoding API 使用 autocomplete=true;同时实现防抖提问: "这是为哪个平台开发的?"
常见答案及其含义:
提问: "当用户选择一个结果时会发生什么?"
常见答案及其含义:
提问: "您预计每月有多少次搜索?"
含义:
根据发现阶段的答案,推荐合适的产品:
在以下情况使用:
产品:
在以下情况使用:
重要提示: 始终优先使用 SDK(Mapbox Search JS、Search SDK for iOS/Android)而不是直接调用 API。SDK 处理防抖、会话令牌、错误处理,并提供 UI 组件。仅针对高级用例使用直接 API 调用。
何时使用: React 应用程序,需要自动完成 UI 组件,最快的实现方式
安装:
npm install @mapbox/search-js-react
完整实现:
import { SearchBox } from '@mapbox/search-js-react';
import mapboxgl from 'mapbox-gl';
function App() {
const [map, setMap] = React.useState(null);
React.useEffect(() => {
mapboxgl.accessToken = 'YOUR_MAPBOX_TOKEN';
const mapInstance = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [-122.4194, 37.7749],
zoom: 12
});
setMap(mapInstance);
}, []);
const handleRetrieve = (result) => {
const [lng, lat] = result.features[0].geometry.coordinates;
map.flyTo({ center: [lng, lat], zoom: 14 });
new mapboxgl.Marker().setLngLat([lng, lat]).addTo(map);
};
return (
<div>
<SearchBox accessToken="YOUR_MAPBOX_TOKEN" onRetrieve={handleRetrieve} placeholder="Search for places" />
<div id="map" style={{ height: '600px' }} />
</div>
);
}
何时使用: 原生 JavaScript、Web 组件或任何框架,需要自动完成 UI
完整实现:
<!DOCTYPE html>
<html>
<head>
<script src="https://api.mapbox.com/search-js/v1.0.0-beta.18/web.js"></script>
<link href="https://api.mapbox.com/search-js/v1.0.0-beta.18/web.css" rel="stylesheet" />
<script src="https://api.mapbox.com/mapbox-gl-js/v3.0.0/mapbox-gl.js"></script>
<link href="https://api.mapbox.com/mapbox-gl-js/v3.0.0/mapbox-gl.css" rel="stylesheet" />
</head>
<body>
<div id="search"></div>
<div id="map" style="height: 600px;"></div>
<script>
// Initialize map
mapboxgl.accessToken = 'YOUR_MAPBOX_TOKEN';
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [-122.4194, 37.7749],
zoom: 12
});
// Initialize Search Box
const search = new MapboxSearchBox();
search.accessToken = 'YOUR_MAPBOX_TOKEN';
// CRITICAL: Set options based on discovery
search.options = {
language: 'en',
country: 'US', // If single-country (from Question 2)
proximity: 'ip', // Or specific coordinates
types: 'address,poi' // Based on Question 1
};
search.mapboxgl = mapboxgl;
search.marker = true; // Auto-add marker on result selection
// Handle result selection
search.addEventListener('retrieve', (event) => {
const result = event.detail;
// Fly to result
map.flyTo({
center: result.geometry.coordinates,
zoom: 15,
essential: true
});
// Optional: Show popup with details
new mapboxgl.Popup()
.setLngLat(result.geometry.coordinates)
.setHTML(
`<h3>${result.properties.name}</h3>
<p>${result.properties.full_address || ''}</p>`
)
.addTo(map);
});
// Attach to DOM
document.getElementById('search').appendChild(search);
</script>
</body>
</html>
关键实现说明:
country(结果更好,成本更低)typesproximity 将结果偏向用户位置retrieve 事件以响应用户选择结果何时使用: 需要自定义 UI 设计,完全控制 UX,适用于任何框架或 Node.js
安装:
npm install @mapbox/search-js-core
完整实现:
import { SearchSession } from '@mapbox/search-js-core';
import mapboxgl from 'mapbox-gl';
// Initialize search session
const search = new SearchSession({
accessToken: 'YOUR_MAPBOX_TOKEN'
});
// Initialize map
mapboxgl.accessToken = 'YOUR_MAPBOX_TOKEN';
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [-122.4194, 37.7749],
zoom: 12
});
// Your custom search input
const searchInput = document.getElementById('search-input');
const resultsContainer = document.getElementById('results');
// Handle user input
searchInput.addEventListener('input', async (e) => {
const query = e.target.value;
if (query.length < 2) {
resultsContainer.innerHTML = '';
return;
}
// Get suggestions (Search JS Core handles debouncing and session tokens)
const response = await search.suggest(query, {
proximity: map.getCenter().toArray(),
country: 'US', // Optional
types: ['address', 'poi']
});
// Render custom results UI
resultsContainer.innerHTML = response.suggestions
.map(
(suggestion) => `
<div class="result-item" data-id="${suggestion.mapbox_id}">
<strong>${suggestion.name}</strong>
<div>${suggestion.place_formatted}</div>
</div>
`
)
.join('');
});
// Handle result selection
resultsContainer.addEventListener('click', async (e) => {
const resultItem = e.target.closest('.result-item');
if (!resultItem) return;
const mapboxId = resultItem.dataset.id;
// Retrieve full details
const result = await search.retrieve(mapboxId);
const feature = result.features[0];
const [lng, lat] = feature.geometry.coordinates;
// Update map
map.flyTo({ center: [lng, lat], zoom: 15 });
new mapboxgl.Marker().setLngLat([lng, lat]).addTo(map);
// Clear search
searchInput.value = feature.properties.name;
resultsContainer.innerHTML = '';
});
主要优势:
何时使用: SDK 不支持的非常特定的需求,或者 Search JS Core 不适合的服务器端集成
重要提示: 仅当 SDK 无法满足您的需求时才使用直接 API 调用。您需要手动处理防抖和会话令牌。
何时使用: 自定义 UI、框架集成,需要完全控制
带防抖的完整实现:
import mapboxgl from 'mapbox-gl';
class MapboxSearch {
constructor(accessToken, options = {}) {
this.accessToken = accessToken;
this.options = {
country: options.country || null, // e.g., 'US'
language: options.language || 'en',
proximity: options.proximity || 'ip',
types: options.types || 'address,poi',
limit: options.limit || 5,
...options
};
this.debounceTimeout = null;
this.sessionToken = this.generateSessionToken();
}
generateSessionToken() {
return `${Date.now()}-${Math.random().toString(36).substring(7)}`;
}
// CRITICAL: Debounce to avoid API spam
async search(query, callback, debounceMs = 300) {
clearTimeout(this.debounceTimeout);
this.debounceTimeout = setTimeout(async () => {
const results = await this.performSearch(query);
callback(results);
}, debounceMs);
}
async performSearch(query) {
if (!query || query.length < 2) return [];
const params = new URLSearchParams({
q: query,
access_token: this.accessToken,
session_token: this.sessionToken,
language: this.options.language,
limit: this.options.limit
});
// Add optional parameters
if (this.options.country) {
params.append('country', this.options.country);
}
if (this.options.types) {
params.append('types', this.options.types);
}
if (this.options.proximity && this.options.proximity !== 'ip') {
params.append('proximity', this.options.proximity);
}
try {
const response = await fetch(`https://api.mapbox.com/search/searchbox/v1/suggest?${params}`);
if (!response.ok) {
throw new Error(`Search API error: ${response.status}`);
}
const data = await response.json();
return data.suggestions || [];
} catch (error) {
console.error('Search error:', error);
return [];
}
}
async retrieve(suggestionId) {
const params = new URLSearchParams({
access_token: this.accessToken,
session_token: this.sessionToken
});
try {
const response = await fetch(`https://api.mapbox.com/search/searchbox/v1/retrieve/${suggestionId}?${params}`);
if (!response.ok) {
throw new Error(`Retrieve API error: ${response.status}`);
}
const data = await response.json();
// Session ends on retrieve - generate new token for next search
this.sessionToken = this.generateSessionToken();
return data.features[0];
} catch (error) {
console.error('Retrieve error:', error);
return null;
}
}
}
// Usage example
const search = new MapboxSearch('YOUR_MAPBOX_TOKEN', {
country: 'US', // Based on discovery Question 2
types: 'poi', // Based on discovery Question 1
proximity: [-122.4194, 37.7749] // Or 'ip' for user location
});
// Attach to input field
const input = document.getElementById('search-input');
const resultsContainer = document.getElementById('search-results');
input.addEventListener('input', (e) => {
const query = e.target.value;
search.search(query, (results) => {
displayResults(results);
});
});
function displayResults(results) {
resultsContainer.innerHTML = results
.map(
(result) => `
<div class="result" data-id="${result.mapbox_id}">
<strong>${result.name}</strong>
<p>${result.place_formatted || ''}</p>
</div>
`
)
.join('');
// Handle result selection
resultsContainer.querySelectorAll('.result').forEach((el) => {
el.addEventListener('click', async () => {
const feature = await search.retrieve(el.dataset.id);
handleResultSelection(feature);
});
});
}
function handleResultSelection(feature) {
const [lng, lat] = feature.geometry.coordinates;
// Fly map to result
map.flyTo({
center: [lng, lat],
zoom: 15
});
// Add marker
new mapboxgl.Marker().setLngLat([lng, lat]).addTo(map);
// Close results
resultsContainer.innerHTML = '';
input.value = feature.properties.name;
}
关键实现细节:
最佳实践: 使用 Search JS React 实现最简单,或使用 Search JS Core 实现自定义 UI。
import { SearchBox } from '@mapbox/search-js-react';
import mapboxgl from 'mapbox-gl';
import { useState } from 'react';
function MapboxSearchComponent() {
const [map, setMap] = useState(null);
useEffect(() => {
const mapInstance = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [-122.4194, 37.7749],
zoom: 12
});
setMap(mapInstance);
}, []);
const handleRetrieve = (result) => {
const [lng, lat] = result.features[0].geometry.coordinates;
map.flyTo({ center: [lng, lat], zoom: 14 });
new mapboxgl.Marker().setLngLat([lng, lat]).addTo(map);
};
return (
<div>
<SearchBox
accessToken="YOUR_MAPBOX_TOKEN"
onRetrieve={handleRetrieve}
placeholder="Search for places"
options={{
country: 'US', // Optional
types: 'address,poi'
}}
/>
<div id="map" style={{ height: '600px' }} />
</div>
);
}
优势:
import { useState, useEffect } from 'react';
import { SearchSession } from '@mapbox/search-js-core';
import mapboxgl from 'mapbox-gl';
function MapboxSearchComponent({ country, types = 'address,poi' }) {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isLoading, setIsLoading] = useState(false);
// Search JS Core handles debouncing and session tokens automatically
const searchSession = new SearchSession({
accessToken: 'YOUR_MAPBOX_TOKEN'
});
useEffect(() => {
const performSearch = async () => {
if (!query || query.length < 2) {
setResults([]);
return;
}
setIsLoading(true);
try {
const response = await searchSession.suggest(query, {
country,
types,
limit: 5
});
setResults(response.suggestions || []);
} catch (error) {
console.error('Search error:', error);
setResults([]);
} finally {
setIsLoading(false);
}
};
performSearch();
}, [query]);
const handleResultClick = async (suggestion) => {
try {
const result = await searchSession.retrieve(suggestion);
const feature = result.features[0];
// Handle result (fly to location, add marker, etc.)
onResultSelect(feature);
setQuery(feature.properties.name);
setResults([]);
} catch (error) {
console.error('Retrieve error:', error);
}
};
return (
<div className="search-container">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search for a place..."
className="search-input"
/>
{isLoading && <div className="loading">Searching...</div>}
{results.length > 0 && (
<div className="search-results">
{results.map((result) => (
<div key={result.mapbox_id} className="search-result" onClick={() => handleResultClick(result)}>
<strong>{result.name}</strong>
{result.place_formatted && <p>{result.place_formatted}</p>}
</div>
))}
</div>
)}
</div>
);
}
优势:
注意: 对于 React 应用,除非需要完全自定义的 UI 设计,否则优先使用 Search JS React(选项 1)。
何时使用: iOS 应用,需要预构建的搜索 UI,最快的实现方式
安装:
// Add to Package.swift or SPM
dependencies: [
.package(url: "https://github.com/mapbox/mapbox-search-ios.git", from: "2.0.0")
]
带内置 UI 的完整实现:
import MapboxSearch
import MapboxMaps
class SearchViewController: UIViewController {
private var searchController: MapboxSearchController!
private var mapView: MapView!
override func viewDidLoad() {
super.viewDidLoad()
setupMap()
setupSearchWithUI()
}
func setupMap() {
mapView = MapView(frame: view.bounds)
view.addSubview(mapView)
}
func setupSearchWithUI() {
// MapboxSearchController provides complete UI automatically
searchController = MapboxSearchController()
searchController.delegate = self
// Present the search UI
present(searchController, animated: true)
}
}
extension SearchViewController: SearchControllerDelegate {
func searchResultSelected(_ searchResult: SearchResult) {
// SDK handled all the search interaction
// Just respond to selection
mapView.camera.fly(to: CameraOptions(
center: searchResult.coordinate,
zoom: 15
))
let annotation = PointAnnotation(coordinate: searchResult.coordinate)
mapView.annotations.pointAnnotations = [annotation]
dismiss(animated: true)
}
}
何时使用: 需要自定义 UI,与 UISearchController 集成,完全控制 UX
完整实现:
import MapboxSearch
import MapboxMaps
class SearchViewController: UIViewController {
private var searchEngine: SearchEngine!
private var mapView: MapView!
override func viewDidLoad() {
super.viewDidLoad()
// Initialize Search Engine (SDK handles debouncing and session tokens)
searchEngine = SearchEngine(accessToken: "YOUR_MAPBOX_TOKEN")
setupSearchBar()
setupMap()
}
func setupSearchBar() {
let searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
navigationItem.searchController = searchController
}
func setupMap() {
mapView = MapView(frame: view.bounds)
view.addSubview(mapView)
}
}
extension SearchViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
guard let query = searchController.searchBar.text, !query.isEmpty else {
return
}
// Search SDK handles debouncing automatically
searchEngine.search(query: query) { [weak self] result in
switch result {
case .success(let results):
self?.displayResults(results)
case .failure(let error):
print("Search error: \(error)")
}
}
}
func displayResults(_ results: [SearchResult]) {
// Display results in custom table view
// When user selects a result:
handleResultSelection(results[0])
}
func handleResultSelection(_ result: SearchResult) {
mapView.camera.fly(to: CameraOptions(
center: result.coordinate,
zoom: 15
))
let annotation = PointAnnotation(coordinate: result.coordinate)
mapView.annotations.pointAnnotations = [annotation]
}
}
何时使用: 非常特定的需求,iOS 服务器端后端
重要提示: 仅当 SDK 无法满足您的需求时才使用。您必须手动处理防抖和会话令牌。
// Direct API calls - see Web direct API example
// Not recommended for iOS - use Search SDK instead
何时使用: Android 应用,需要预构建的搜索 UI,最快的实现方式
安装:
// Add to build.gradle
dependencies {
implementation 'com.mapbox.search:mapbox-search-android-ui:2.0.0'
implementation 'com.mapbox.maps:android:11.0.0'
}
带内置 UI 的完整实现:
import com.mapbox.search.ui.view.SearchBottomSheetView
import com.mapbox.maps.MapView
class SearchActivity : AppCompatActivity() {
private lateinit var searchView: SearchBottomSheetView
private lateinit var mapView: MapView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search)
mapView = findViewById(R.id.map_view)
// SearchBottomSheetView provides complete UI automatically
searchView = findViewById(R.id.search_view)
searchView.initializeSearch(
savedInstanceState,
SearchBottomSheetView.Configuration()
)
// Handle result selection
searchView.addOnSearchResultClickListener { searchResult ->
// SDK handled all the search interaction
val coordinate = searchResult.coordinate
mapView.getMapboxMap().flyTo(
CameraOptions.Builder()
.center(Point.fromLngLat(coordinate.longitude, coordinate.latitude))
.zoom(15.0)
.build()
)
searchView.hide()
}
}
}
何时使用: 需要自定义 UI,与 SearchView 集成,完全控制 UX
完整实现:
import com.mapbox.search.SearchEngine
import com.mapbox.search.SearchEngineSettings
import com.mapbox.search.SearchOptions
import com.mapbox.maps.MapView
class SearchActivity : AppCompatActivity() {
private lateinit var searchEngine: SearchEngine
private lateinit var mapView: MapView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Initialize Search Engine (SDK handles debouncing and session tokens)
searchEngine = SearchEngine.createSearchEngine(
SearchEngineSettings("YOUR_MAPBOX_TOKEN")
)
setupSearchView()
setupMap()
}
private fun setupSearchView() {
val searchView = findViewById<SearchView>(R.id.search_view)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
performSearch(query)
return true
}
override fun onQueryTextChange(newText: String): Boolean {
if (newText.length >= 2) {
// Search SDK handles debouncing automatically
performSearch(newText)
}
return true
}
})
}
private fun performSearch(query: String) {
val options = SearchOptions(
countries = listOf("US"),
limit = 5
)
searchEngine.search(query, options) { results ->
results.onSuccess { searchResults ->
displayResults(searchResults)
}.onFailure { error ->
Log.e("Search", "Error: $error")
}
}
}
private fun displayResults(results: List<SearchResult>) {
// Display in custom RecyclerView
handleResultSelection(results[0])
}
private fun handleResultSelection(result: SearchResult) {
val coordinate = result.coordinate
mapView.getMapboxMap().flyTo(
CameraOptions.Builder()
.center(Point.fromLngLat(coordinate.longitude, coordinate.latitude))
.zoom(15.0)
.build()
)
}
}
何时使用: 非常特定的需求,Android 服务器端后端
重要提示: 仅当 SDK 无法满足您的需求时才使用。您必须手动处理防抖和会话令牌。
// Direct API calls - see Web direct API example
// Not recommended for Android - use Search SDK instead
何时使用: 服务器端搜索、后端 API、无服务器函数
安装:
npm install @mapbox/search-js-core
完整实现:
import { SearchSession } from '@mapbox/search-js-core';
// Initialize search session (handles session tokens automatically)
const search = new SearchSession({
accessToken: process.env.MAPBOX_TOKEN
});
// Express.js API endpoint example
app.get('/api/search', async (req, res) => {
const { query, proximity, country } = req.query;
try {
// Get suggestions (Search JS Core handles session management)
const response = await search.suggest(query, {
proximity: proximity ? proximity.split(',').map(Number) : undefined,
country: country,
limit: 10
});
res.json(response.suggestions);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Retrieve full details for a selected result
app.get('/api/search/:id', async (req, res) => {
try {
const result = await search.retrieve(req.params.id);
res.json(result.features[0]);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
主要优势:
何时使用: 非常特定的需求,需要 Search JS Core 中没有的功能
实现:
import fetch from 'node-fetch';
async function searchPlaces(query, options = {}) {
const params = new URLSearchParams({
q: query,
access_token: process.env.MAPBOX_TOKEN,
session_token: generateSessionToken(), // You must manage this
...options
});
const response = await fetch(`https://api.mapbox.com/search/searchbox/v1/suggest?${params}`);
return response.json();
}
重要提示: 仅当 Search JS Core 无法满足您的需求时才使用直接 API 调用。您需要手动处理会话令牌。
注意: 防抖仅在直接调用 API 时才需要考虑。Mapbox Search JS 和 Search SDK 会自动处理防抖。
问题: 每次按键 = API 调用 = 昂贵 + 缓慢
解决方案: 等待用户停止输入(针对直接 API 集成)
let debounceTimeout;
function debouncedSearch(query) {
clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(() => {
performSearch(query);
}, 300); // 300ms is optimal for most use cases
}
为什么是 300ms?
注意: 会话令牌仅在直接调用 API 时才需要考虑。Mapbox Search JS 和 iOS/Android 的 Search SDK 会自动处理会话令牌。
问题: Search Box API 按会话收费,而非按请求收费
什么是会话?
实现(仅限直接 API 调用):
class SearchSession {
constructor() {
this.token = this.generateToken();
}
generateToken() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
async suggest(query) {
// Use this.token for all suggest requests
return fetch(`...?session_token=${this.token}`);
}
async retrieve(id) {
const result = await fetch(`...?session_token=${this.token}`);
// Session ends - generate new token
this.token = this.generateToken();
return result;
}
}
成本影响:
尽可能设置位置上下文:
// GOOD: Specific country
{
country: 'US';
}
// GOOD: Proximity to user
{
proximity: [-122.4194, 37.7749];
}
// GOOD: Bounding box for service area
{
bbox: [-122.5, 37.7, -122.3, 37.9];
}
// BAD: No geographic context
{
} // Returns global results, slower, less relevant
提示: 使用 Location Helper 工具 轻松计算服务区域的边界框。
为什么重要:
Expert guidance for implementing Mapbox search functionality in applications. Covers the complete workflow from asking the right discovery questions, selecting the appropriate search product, to implementing production-ready integrations following best practices from the Mapbox search team.
User says things like:
This skill complementsmapbox-search-patterns:
mapbox-search-patterns = Tool and parameter selectionmapbox-search-integration = Complete implementation workflowBefore jumping into code, ask these questions to understand requirements:
Ask: "What do you want users to search for?"
Common answers and implications:
Follow-up if not stated initially : "Are your users searching for points of interest data? Restaurants, stores, categories of businesses?"
Implications:
Ask: "Where will users be searching?"
Common answers and implications:
country parameter, better results, lower costbbox parameter for bounding box constraintcountry array parameterFollow-up: "Do you need to limit results to a specific area?" (delivery zone, service area, etc.)
Ask: "How will users interact with search?"
Common answers and implications:
auto_complete: true, for Search Box API, or autocomplete=true for Geocoding; also implement debouncingAsk: "What platform is this for?"
Common answers and implications:
Ask: "What happens when a user selects a result?"
Common answers and implications:
Ask: "How many searches do you expect per month?"
Implications:
Based on discovery answers, recommend the right product:
Use when:
Products:*
Use when:
Important: Always prefer using SDKs (Mapbox Search JS, Search SDK for iOS/Android) over calling APIs directly. SDKs handle debouncing, session tokens, error handling, and provide UI components. Only use direct API calls for advanced use cases.
When to use: React application, want autocomplete UI component, fastest implementation
Installation:
npm install @mapbox/search-js-react
Complete implementation:
import { SearchBox } from '@mapbox/search-js-react';
import mapboxgl from 'mapbox-gl';
function App() {
const [map, setMap] = React.useState(null);
React.useEffect(() => {
mapboxgl.accessToken = 'YOUR_MAPBOX_TOKEN';
const mapInstance = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [-122.4194, 37.7749],
zoom: 12
});
setMap(mapInstance);
}, []);
const handleRetrieve = (result) => {
const [lng, lat] = result.features[0].geometry.coordinates;
map.flyTo({ center: [lng, lat], zoom: 14 });
new mapboxgl.Marker().setLngLat([lng, lat]).addTo(map);
};
return (
<div>
<SearchBox accessToken="YOUR_MAPBOX_TOKEN" onRetrieve={handleRetrieve} placeholder="Search for places" />
<div id="map" style={{ height: '600px' }} />
</div>
);
}
When to use: Vanilla JavaScript, Web Components, or any framework, want autocomplete UI
Complete implementation:
<!DOCTYPE html>
<html>
<head>
<script src="https://api.mapbox.com/search-js/v1.0.0-beta.18/web.js"></script>
<link href="https://api.mapbox.com/search-js/v1.0.0-beta.18/web.css" rel="stylesheet" />
<script src="https://api.mapbox.com/mapbox-gl-js/v3.0.0/mapbox-gl.js"></script>
<link href="https://api.mapbox.com/mapbox-gl-js/v3.0.0/mapbox-gl.css" rel="stylesheet" />
</head>
<body>
<div id="search"></div>
<div id="map" style="height: 600px;"></div>
<script>
// Initialize map
mapboxgl.accessToken = 'YOUR_MAPBOX_TOKEN';
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [-122.4194, 37.7749],
zoom: 12
});
// Initialize Search Box
const search = new MapboxSearchBox();
search.accessToken = 'YOUR_MAPBOX_TOKEN';
// CRITICAL: Set options based on discovery
search.options = {
language: 'en',
country: 'US', // If single-country (from Question 2)
proximity: 'ip', // Or specific coordinates
types: 'address,poi' // Based on Question 1
};
search.mapboxgl = mapboxgl;
search.marker = true; // Auto-add marker on result selection
// Handle result selection
search.addEventListener('retrieve', (event) => {
const result = event.detail;
// Fly to result
map.flyTo({
center: result.geometry.coordinates,
zoom: 15,
essential: true
});
// Optional: Show popup with details
new mapboxgl.Popup()
.setLngLat(result.geometry.coordinates)
.setHTML(
`<h3>${result.properties.name}</h3>
<p>${result.properties.full_address || ''}</p>`
)
.addTo(map);
});
// Attach to DOM
document.getElementById('search').appendChild(search);
</script>
</body>
</html>
Key implementation notes:
country if single-country search (better results, lower cost)types based on what users search forproximity to bias results to user locationretrieve event for result selectionWhen to use: Need custom UI design, full control over UX, works in any framework or Node.js
Installation:
npm install @mapbox/search-js-core
Complete implementation:
import { SearchSession } from '@mapbox/search-js-core';
import mapboxgl from 'mapbox-gl';
// Initialize search session
const search = new SearchSession({
accessToken: 'YOUR_MAPBOX_TOKEN'
});
// Initialize map
mapboxgl.accessToken = 'YOUR_MAPBOX_TOKEN';
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [-122.4194, 37.7749],
zoom: 12
});
// Your custom search input
const searchInput = document.getElementById('search-input');
const resultsContainer = document.getElementById('results');
// Handle user input
searchInput.addEventListener('input', async (e) => {
const query = e.target.value;
if (query.length < 2) {
resultsContainer.innerHTML = '';
return;
}
// Get suggestions (Search JS Core handles debouncing and session tokens)
const response = await search.suggest(query, {
proximity: map.getCenter().toArray(),
country: 'US', // Optional
types: ['address', 'poi']
});
// Render custom results UI
resultsContainer.innerHTML = response.suggestions
.map(
(suggestion) => `
<div class="result-item" data-id="${suggestion.mapbox_id}">
<strong>${suggestion.name}</strong>
<div>${suggestion.place_formatted}</div>
</div>
`
)
.join('');
});
// Handle result selection
resultsContainer.addEventListener('click', async (e) => {
const resultItem = e.target.closest('.result-item');
if (!resultItem) return;
const mapboxId = resultItem.dataset.id;
// Retrieve full details
const result = await search.retrieve(mapboxId);
const feature = result.features[0];
const [lng, lat] = feature.geometry.coordinates;
// Update map
map.flyTo({ center: [lng, lat], zoom: 15 });
new mapboxgl.Marker().setLngLat([lng, lat]).addTo(map);
// Clear search
searchInput.value = feature.properties.name;
resultsContainer.innerHTML = '';
});
Key benefits:
When to use: Very specific requirements that SDKs don't support, or server-side integration where Search JS Core doesn't fit
Important: Only use direct API calls when SDKs don't meet your needs. You'll need to handle debouncing and session tokens manually.
When to use: Custom UI, framework integration, need full control
Complete implementation with debouncing:
import mapboxgl from 'mapbox-gl';
class MapboxSearch {
constructor(accessToken, options = {}) {
this.accessToken = accessToken;
this.options = {
country: options.country || null, // e.g., 'US'
language: options.language || 'en',
proximity: options.proximity || 'ip',
types: options.types || 'address,poi',
limit: options.limit || 5,
...options
};
this.debounceTimeout = null;
this.sessionToken = this.generateSessionToken();
}
generateSessionToken() {
return `${Date.now()}-${Math.random().toString(36).substring(7)}`;
}
// CRITICAL: Debounce to avoid API spam
async search(query, callback, debounceMs = 300) {
clearTimeout(this.debounceTimeout);
this.debounceTimeout = setTimeout(async () => {
const results = await this.performSearch(query);
callback(results);
}, debounceMs);
}
async performSearch(query) {
if (!query || query.length < 2) return [];
const params = new URLSearchParams({
q: query,
access_token: this.accessToken,
session_token: this.sessionToken,
language: this.options.language,
limit: this.options.limit
});
// Add optional parameters
if (this.options.country) {
params.append('country', this.options.country);
}
if (this.options.types) {
params.append('types', this.options.types);
}
if (this.options.proximity && this.options.proximity !== 'ip') {
params.append('proximity', this.options.proximity);
}
try {
const response = await fetch(`https://api.mapbox.com/search/searchbox/v1/suggest?${params}`);
if (!response.ok) {
throw new Error(`Search API error: ${response.status}`);
}
const data = await response.json();
return data.suggestions || [];
} catch (error) {
console.error('Search error:', error);
return [];
}
}
async retrieve(suggestionId) {
const params = new URLSearchParams({
access_token: this.accessToken,
session_token: this.sessionToken
});
try {
const response = await fetch(`https://api.mapbox.com/search/searchbox/v1/retrieve/${suggestionId}?${params}`);
if (!response.ok) {
throw new Error(`Retrieve API error: ${response.status}`);
}
const data = await response.json();
// Session ends on retrieve - generate new token for next search
this.sessionToken = this.generateSessionToken();
return data.features[0];
} catch (error) {
console.error('Retrieve error:', error);
return null;
}
}
}
// Usage example
const search = new MapboxSearch('YOUR_MAPBOX_TOKEN', {
country: 'US', // Based on discovery Question 2
types: 'poi', // Based on discovery Question 1
proximity: [-122.4194, 37.7749] // Or 'ip' for user location
});
// Attach to input field
const input = document.getElementById('search-input');
const resultsContainer = document.getElementById('search-results');
input.addEventListener('input', (e) => {
const query = e.target.value;
search.search(query, (results) => {
displayResults(results);
});
});
function displayResults(results) {
resultsContainer.innerHTML = results
.map(
(result) => `
<div class="result" data-id="${result.mapbox_id}">
<strong>${result.name}</strong>
<p>${result.place_formatted || ''}</p>
</div>
`
)
.join('');
// Handle result selection
resultsContainer.querySelectorAll('.result').forEach((el) => {
el.addEventListener('click', async () => {
const feature = await search.retrieve(el.dataset.id);
handleResultSelection(feature);
});
});
}
function handleResultSelection(feature) {
const [lng, lat] = feature.geometry.coordinates;
// Fly map to result
map.flyTo({
center: [lng, lat],
zoom: 15
});
// Add marker
new mapboxgl.Marker().setLngLat([lng, lat]).addTo(map);
// Close results
resultsContainer.innerHTML = '';
input.value = feature.properties.name;
}
Critical implementation details:
Best Practice: Use Search JS React for easiest implementation, or Search JS Core for custom UI.
import { SearchBox } from '@mapbox/search-js-react';
import mapboxgl from 'mapbox-gl';
import { useState } from 'react';
function MapboxSearchComponent() {
const [map, setMap] = useState(null);
useEffect(() => {
const mapInstance = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [-122.4194, 37.7749],
zoom: 12
});
setMap(mapInstance);
}, []);
const handleRetrieve = (result) => {
const [lng, lat] = result.features[0].geometry.coordinates;
map.flyTo({ center: [lng, lat], zoom: 14 });
new mapboxgl.Marker().setLngLat([lng, lat]).addTo(map);
};
return (
<div>
<SearchBox
accessToken="YOUR_MAPBOX_TOKEN"
onRetrieve={handleRetrieve}
placeholder="Search for places"
options={{
country: 'US', // Optional
types: 'address,poi'
}}
/>
<div id="map" style={{ height: '600px' }} />
</div>
);
}
Benefits:
import { useState, useEffect } from 'react';
import { SearchSession } from '@mapbox/search-js-core';
import mapboxgl from 'mapbox-gl';
function MapboxSearchComponent({ country, types = 'address,poi' }) {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isLoading, setIsLoading] = useState(false);
// Search JS Core handles debouncing and session tokens automatically
const searchSession = new SearchSession({
accessToken: 'YOUR_MAPBOX_TOKEN'
});
useEffect(() => {
const performSearch = async () => {
if (!query || query.length < 2) {
setResults([]);
return;
}
setIsLoading(true);
try {
const response = await searchSession.suggest(query, {
country,
types,
limit: 5
});
setResults(response.suggestions || []);
} catch (error) {
console.error('Search error:', error);
setResults([]);
} finally {
setIsLoading(false);
}
};
performSearch();
}, [query]);
const handleResultClick = async (suggestion) => {
try {
const result = await searchSession.retrieve(suggestion);
const feature = result.features[0];
// Handle result (fly to location, add marker, etc.)
onResultSelect(feature);
setQuery(feature.properties.name);
setResults([]);
} catch (error) {
console.error('Retrieve error:', error);
}
};
return (
<div className="search-container">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search for a place..."
className="search-input"
/>
{isLoading && <div className="loading">Searching...</div>}
{results.length > 0 && (
<div className="search-results">
{results.map((result) => (
<div key={result.mapbox_id} className="search-result" onClick={() => handleResultClick(result)}>
<strong>{result.name}</strong>
{result.place_formatted && <p>{result.place_formatted}</p>}
</div>
))}
</div>
)}
</div>
);
}
Benefits:
Note: For React apps, prefer Search JS React (Option 1) unless you need a completely custom UI design.
When to use: iOS app, want pre-built search UI, fastest implementation
Installation:
// Add to Package.swift or SPM
dependencies: [
.package(url: "https://github.com/mapbox/mapbox-search-ios.git", from: "2.0.0")
]
Complete implementation with built-in UI:
import MapboxSearch
import MapboxMaps
class SearchViewController: UIViewController {
private var searchController: MapboxSearchController!
private var mapView: MapView!
override func viewDidLoad() {
super.viewDidLoad()
setupMap()
setupSearchWithUI()
}
func setupMap() {
mapView = MapView(frame: view.bounds)
view.addSubview(mapView)
}
func setupSearchWithUI() {
// MapboxSearchController provides complete UI automatically
searchController = MapboxSearchController()
searchController.delegate = self
// Present the search UI
present(searchController, animated: true)
}
}
extension SearchViewController: SearchControllerDelegate {
func searchResultSelected(_ searchResult: SearchResult) {
// SDK handled all the search interaction
// Just respond to selection
mapView.camera.fly(to: CameraOptions(
center: searchResult.coordinate,
zoom: 15
))
let annotation = PointAnnotation(coordinate: searchResult.coordinate)
mapView.annotations.pointAnnotations = [annotation]
dismiss(animated: true)
}
}
When to use: Need custom UI, integrate with UISearchController, full control over UX
Complete implementation:
import MapboxSearch
import MapboxMaps
class SearchViewController: UIViewController {
private var searchEngine: SearchEngine!
private var mapView: MapView!
override func viewDidLoad() {
super.viewDidLoad()
// Initialize Search Engine (SDK handles debouncing and session tokens)
searchEngine = SearchEngine(accessToken: "YOUR_MAPBOX_TOKEN")
setupSearchBar()
setupMap()
}
func setupSearchBar() {
let searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
navigationItem.searchController = searchController
}
func setupMap() {
mapView = MapView(frame: view.bounds)
view.addSubview(mapView)
}
}
extension SearchViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
guard let query = searchController.searchBar.text, !query.isEmpty else {
return
}
// Search SDK handles debouncing automatically
searchEngine.search(query: query) { [weak self] result in
switch result {
case .success(let results):
self?.displayResults(results)
case .failure(let error):
print("Search error: \(error)")
}
}
}
func displayResults(_ results: [SearchResult]) {
// Display results in custom table view
// When user selects a result:
handleResultSelection(results[0])
}
func handleResultSelection(_ result: SearchResult) {
mapView.camera.fly(to: CameraOptions(
center: result.coordinate,
zoom: 15
))
let annotation = PointAnnotation(coordinate: result.coordinate)
mapView.annotations.pointAnnotations = [annotation]
}
}
When to use: Very specific requirements, server-side iOS backend
Important: Only use if SDK doesn't meet your needs. You must handle debouncing and session tokens manually.
// Direct API calls - see Web direct API example
// Not recommended for iOS - use Search SDK instead
When to use: Android app, want pre-built search UI, fastest implementation
Installation:
// Add to build.gradle
dependencies {
implementation 'com.mapbox.search:mapbox-search-android-ui:2.0.0'
implementation 'com.mapbox.maps:android:11.0.0'
}
Complete implementation with built-in UI:
import com.mapbox.search.ui.view.SearchBottomSheetView
import com.mapbox.maps.MapView
class SearchActivity : AppCompatActivity() {
private lateinit var searchView: SearchBottomSheetView
private lateinit var mapView: MapView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search)
mapView = findViewById(R.id.map_view)
// SearchBottomSheetView provides complete UI automatically
searchView = findViewById(R.id.search_view)
searchView.initializeSearch(
savedInstanceState,
SearchBottomSheetView.Configuration()
)
// Handle result selection
searchView.addOnSearchResultClickListener { searchResult ->
// SDK handled all the search interaction
val coordinate = searchResult.coordinate
mapView.getMapboxMap().flyTo(
CameraOptions.Builder()
.center(Point.fromLngLat(coordinate.longitude, coordinate.latitude))
.zoom(15.0)
.build()
)
searchView.hide()
}
}
}
When to use: Need custom UI, integrate with SearchView, full control over UX
Complete implementation:
import com.mapbox.search.SearchEngine
import com.mapbox.search.SearchEngineSettings
import com.mapbox.search.SearchOptions
import com.mapbox.maps.MapView
class SearchActivity : AppCompatActivity() {
private lateinit var searchEngine: SearchEngine
private lateinit var mapView: MapView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Initialize Search Engine (SDK handles debouncing and session tokens)
searchEngine = SearchEngine.createSearchEngine(
SearchEngineSettings("YOUR_MAPBOX_TOKEN")
)
setupSearchView()
setupMap()
}
private fun setupSearchView() {
val searchView = findViewById<SearchView>(R.id.search_view)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
performSearch(query)
return true
}
override fun onQueryTextChange(newText: String): Boolean {
if (newText.length >= 2) {
// Search SDK handles debouncing automatically
performSearch(newText)
}
return true
}
})
}
private fun performSearch(query: String) {
val options = SearchOptions(
countries = listOf("US"),
limit = 5
)
searchEngine.search(query, options) { results ->
results.onSuccess { searchResults ->
displayResults(searchResults)
}.onFailure { error ->
Log.e("Search", "Error: $error")
}
}
}
private fun displayResults(results: List<SearchResult>) {
// Display in custom RecyclerView
handleResultSelection(results[0])
}
private fun handleResultSelection(result: SearchResult) {
val coordinate = result.coordinate
mapView.getMapboxMap().flyTo(
CameraOptions.Builder()
.center(Point.fromLngLat(coordinate.longitude, coordinate.latitude))
.zoom(15.0)
.build()
)
}
}
When to use: Very specific requirements, server-side Android backend
Important: Only use if SDK doesn't meet your needs. You must handle debouncing and session tokens manually.
// Direct API calls - see Web direct API example
// Not recommended for Android - use Search SDK instead
When to use: Server-side search, backend API, serverless functions
Installation:
npm install @mapbox/search-js-core
Complete implementation:
import { SearchSession } from '@mapbox/search-js-core';
// Initialize search session (handles session tokens automatically)
const search = new SearchSession({
accessToken: process.env.MAPBOX_TOKEN
});
// Express.js API endpoint example
app.get('/api/search', async (req, res) => {
const { query, proximity, country } = req.query;
try {
// Get suggestions (Search JS Core handles session management)
const response = await search.suggest(query, {
proximity: proximity ? proximity.split(',').map(Number) : undefined,
country: country,
limit: 10
});
res.json(response.suggestions);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Retrieve full details for a selected result
app.get('/api/search/:id', async (req, res) => {
try {
const result = await search.retrieve(req.params.id);
res.json(result.features[0]);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Key benefits:
When to use: Very specific requirements, need features not in Search JS Core
Implementation:
import fetch from 'node-fetch';
async function searchPlaces(query, options = {}) {
const params = new URLSearchParams({
q: query,
access_token: process.env.MAPBOX_TOKEN,
session_token: generateSessionToken(), // You must manage this
...options
});
const response = await fetch(`https://api.mapbox.com/search/searchbox/v1/suggest?${params}`);
return response.json();
}
Important: Only use direct API calls if Search JS Core doesn't meet your needs. You'll need to handle session tokens manually.
Note: Debouncing is only a concern if you are calling the API directly. Mapbox Search JS and the Search SDKs handle debouncing automatically.
Problem: Every keystroke = API call = expensive + slow
Solution: Wait until user stops typing (for direct API integration)
let debounceTimeout;
function debouncedSearch(query) {
clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(() => {
performSearch(query);
}, 300); // 300ms is optimal for most use cases
}
Why 300ms?
Note: Session tokens are only a concern if you are calling the API directly. Mapbox Search JS and the Search SDKs for iOS/Android handle session tokens automatically.
Problem: Search Box API charges per session, not per request
What's a session?
Implementation (direct API calls only):
class SearchSession {
constructor() {
this.token = this.generateToken();
}
generateToken() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
async suggest(query) {
// Use this.token for all suggest requests
return fetch(`...?session_token=${this.token}`);
}
async retrieve(id) {
const result = await fetch(`...?session_token=${this.token}`);
// Session ends - generate new token
this.token = this.generateToken();
return result;
}
}
Cost impact:
Always set location context when possible:
// GOOD: Specific country
{
country: 'US';
}
// GOOD: Proximity to user
{
proximity: [-122.4194, 37.7749];
}
// GOOD: Bounding box for service area
{
bbox: [-122.5, 37.7, -122.3, 37.9];
}
// BAD: No geographic context
{
} // Returns global results, slower, less relevant
Tip: Use the Location Helper tool to easily calculate bounding boxes for your service area.
Why it matters:
Handle all failure cases:
async function performSearch(query) {
try {
const response = await fetch(searchUrl);
// Check HTTP status
if (!response.ok) {
if (response.status === 429) {
// Rate limited
showError('Too many requests. Please wait a moment.');
return [];
} else if (response.status === 401) {
// Invalid token
showError('Search is unavailable. Please check configuration.');
return [];
} else {
// Other error
showError('Search failed. Please try again.');
return [];
}
}
const data = await response.json();
// Check for results
if (!data.suggestions || data.suggestions.length === 0) {
showMessage('No results found. Try a different search.');
return [];
}
return data.suggestions;
} catch (error) {
// Network error
console.error('Search error:', error);
showError('Network error. Please check your connection.');
return [];
}
}
Show enough context for disambiguation:
<div class="search-result">
<div class="result-name">Starbucks</div>
<div class="result-address">123 Main St, San Francisco, CA</div>
<div class="result-type">Coffee Shop</div>
</div>
Not just:
<div>Starbucks</div>
<!-- Which Starbucks? -->
Always show loading feedback:
function performSearch(query) {
showLoadingSpinner();
fetch(searchUrl)
.then((response) => response.json())
.then((data) => {
hideLoadingSpinner();
displayResults(data.suggestions);
})
.catch((error) => {
hideLoadingSpinner();
showError('Search failed');
});
}
Make search keyboard-navigable:
<input type="search" role="combobox" aria-autocomplete="list" aria-controls="search-results" aria-expanded="false" />
<ul id="search-results" role="listbox">
<li role="option" tabindex="0">Result 1</li>
<li role="option" tabindex="0">Result 2</li>
</ul>
Keyboard support:
iOS/Android specific considerations:
// iOS: Adjust for keyboard
NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillShowNotification,
object: nil,
queue: .main
) { notification in
// Adjust view for keyboard
}
// Handle tap outside to dismiss
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
view.addGestureRecognizer(tapGesture)
Make touch targets large enough:
Cache recent/popular searches:
class SearchCache {
constructor(maxSize = 50) {
this.cache = new Map();
this.maxSize = maxSize;
}
get(query) {
const key = query.toLowerCase();
return this.cache.get(key);
}
set(query, results) {
const key = query.toLowerCase();
// LRU eviction
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, {
results,
timestamp: Date.now()
});
}
isValid(entry, maxAgeMs = 5 * 60 * 1000) {
return entry && Date.now() - entry.timestamp < maxAgeMs;
}
}
// Usage
const cache = new SearchCache();
async function search(query) {
const cached = cache.get(query);
if (cache.isValid(cached)) {
return cached.results;
}
const results = await performAPISearch(query);
cache.set(query, results);
return results;
}
CRITICAL: Scope tokens properly:
// Create token with only search scopes
// In Mapbox dashboard or via API:
{
"scopes": [
"search:read",
"styles:read", // Only if showing map
"fonts:read" // Only if showing map
],
"allowedUrls": [
"https://yourdomain.com/*"
]
}
Never:
See mapbox-token-security skill for details.
Problem:
input.addEventListener('input', (e) => {
performSearch(e.target.value); // API call on EVERY keystroke!
});
Impact:
Solution: Always debounce (see Best Practice #1)
Problem:
// No session token = each request charged separately
fetch('...suggest?q=query&access_token=xxx');
Impact:
Solution: Use session tokens (see Best Practice #2)
Problem:
// Searching globally for "Paris"
{
q: 'Paris';
} // Paris, France? Paris, Texas? Paris, Kentucky?
Impact:
Solution:
// Much better
{ q: 'Paris', country: 'US', proximity: user_location }
Problem:
<!-- Tiny touch targets -->
<div style="height: 20px; padding: 2px;">Search result</div>
Impact:
Solution:
.search-result {
min-height: 48px; /* Android minimum */
padding: 12px;
margin: 4px 0;
}
Problem:
// Just shows empty container
displayResults([]); // User sees blank space - is it loading? broken?
Impact:
Solution:
if (results.length === 0) {
showMessage('No results found. Try a different search term.');
}
Problem:
// No timeout = waits forever on slow network
await fetch(searchUrl);
Impact:
Solution:
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
fetch(searchUrl, { signal: controller.signal }).finally(() => clearTimeout(timeout));
Problem:
// Treating all results the same
displayResult(result.name); // But is it an address? POI? Region?
Impact:
Solution:
function handleResult(result) {
const type = result.feature_type;
if (type === 'poi') {
map.flyTo({ center: coords, zoom: 17 }); // Close zoom
addPOIMarker(result);
} else if (type === 'address') {
map.flyTo({ center: coords, zoom: 16 });
addAddressMarker(result);
} else if (type === 'place') {
map.flyTo({ center: coords, zoom: 12 }); // Wider view for city
}
}
Problem:
// Fast typing: "san francisco"
// API responses arrive out of order:
// "san f" results arrive AFTER "san francisco" results
Impact:
Solution:
let searchCounter = 0;
async function performSearch(query) {
const currentSearch = ++searchCounter;
const results = await fetchResults(query);
// Only display if this is still the latest search
if (currentSearch === searchCounter) {
displayResults(results);
}
}
Best Practice: Use Search JS React or Search JS Core instead of building custom hooks with direct API calls.
import { SearchBox } from '@mapbox/search-js-react';
// Easiest - just use the SearchBox component
function MyComponent() {
return (
<SearchBox
accessToken="YOUR_TOKEN"
onRetrieve={(result) => {
// Handle result
}}
options={{
country: 'US',
types: 'address,poi'
}}
/>
);
}
import { useState, useCallback, useRef, useEffect } from 'react';
import { SearchSession } from '@mapbox/search-js-core';
// Custom hook using Search JS Core (handles debouncing and session tokens)
function useMapboxSearch(accessToken, options = {}) {
const [results, setResults] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
// Search JS Core handles session tokens automatically
const searchSessionRef = useRef(null);
useEffect(() => {
searchSessionRef.current = new SearchSession({ accessToken });
}, [accessToken]);
const search = useCallback(
async (query) => {
if (!query || query.length < 2) {
setResults([]);
return;
}
setIsLoading(true);
setError(null);
try {
// Search JS Core handles debouncing and session tokens
const response = await searchSessionRef.current.suggest(query, options);
setResults(response.suggestions || []);
} catch (err) {
setError(err.message);
setResults([]);
} finally {
setIsLoading(false);
}
},
[options]
);
const retrieve = useCallback(async (suggestion) => {
try {
// Search JS Core handles session tokens automatically
const result = await searchSessionRef.current.retrieve(suggestion);
return result.features[0];
} catch (err) {
setError(err.message);
throw err;
}
}, []);
return { results, isLoading, error, search, retrieve };
}
Benefits of using Search JS Core:
import { ref, watch } from 'vue';
import { SearchSession } from '@mapbox/search-js-core';
export function useMapboxSearch(accessToken, options = {}) {
const query = ref('');
const results = ref([]);
const isLoading = ref(false);
// Use Search JS Core - handles debouncing and session tokens automatically
const searchSession = new SearchSession({ accessToken });
const performSearch = async (searchQuery) => {
if (!searchQuery || searchQuery.length < 2) {
results.value = [];
return;
}
isLoading.value = true;
try {
// Search JS Core handles debouncing and session tokens
const response = await searchSession.suggest(searchQuery, options);
results.value = response.suggestions || [];
} catch (error) {
console.error('Search error:', error);
results.value = [];
} finally {
isLoading.value = false;
}
};
// Watch query changes (Search JS Core handles debouncing)
watch(query, (newQuery) => {
performSearch(newQuery);
});
const retrieve = async (suggestion) => {
// Search JS Core handles session tokens automatically
const feature = await searchSession.retrieve(suggestion);
return feature;
};
return {
query,
results,
isLoading,
retrieve
};
}
Key benefits:
// Mock fetch for testing
global.fetch = jest.fn();
describe('MapboxSearch', () => {
beforeEach(() => {
fetch.mockClear();
});
test('debounces search requests', async () => {
const search = new MapboxSearch('fake-token');
// Rapid-fire searches
search.search('san');
search.search('san f');
search.search('san fr');
search.search('san francisco');
// Wait for debounce
await new Promise((resolve) => setTimeout(resolve, 400));
// Should only make one API call
expect(fetch).toHaveBeenCalledTimes(1);
});
test('handles empty results', async () => {
fetch.mockResolvedValue({
ok: true,
json: async () => ({ suggestions: [] })
});
const search = new MapboxSearch('fake-token');
const results = await search.performSearch('xyz');
expect(results).toEqual([]);
});
test('handles API errors', async () => {
fetch.mockResolvedValue({
ok: false,
status: 429
});
const search = new MapboxSearch('fake-token');
const results = await search.performSearch('test');
expect(results).toEqual([]);
});
});
describe('Search Integration', () => {
test('complete search flow', async () => {
const search = new MapboxSearch(process.env.MAPBOX_TOKEN);
// Perform search
const suggestions = await search.performSearch('San Francisco');
expect(suggestions.length).toBeGreaterThan(0);
// Retrieve first result
const feature = await search.retrieve(suggestions[0].mapbox_id);
expect(feature.geometry.coordinates).toBeDefined();
expect(feature.properties.name).toBe('San Francisco');
});
});
// Track search usage
function trackSearch(query, resultsCount) {
analytics.track('search_performed', {
query_length: query.length,
results_count: resultsCount,
had_results: resultsCount > 0
});
}
// Track selections
function trackSelection(result, position) {
analytics.track('search_result_selected', {
result_type: result.feature_type,
result_position: position,
had_address: !!result.properties.full_address
});
}
// Track errors
function trackError(errorType, query) {
analytics.track('search_error', {
error_type: errorType,
query_length: query.length
});
}
Before launching, verify:
Configuration:
Implementation:
UX:
Performance:
Testing:
Monitoring:
Works with:
User says: "I need location search"
Remember: The best search implementation asks the right questions first, then builds exactly what the user needs - no more, no less.
Weekly Installs
266
Repository
GitHub Stars
35
First Seen
Feb 5, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
gemini-cli247
opencode246
codex242
github-copilot240
kimi-cli231
amp230
agent-browser 浏览器自动化工具 - Vercel Labs 命令行网页操作与测试
140,500 周安装