Integrating search into a web application
This section provides an example of integrating map-based search into your web application. The interactive example demonstrates how to use the core 2GIS search APIs:
- Geocoder API to determine an address from map click coordinates
- Markers API to search for places within a given radius and display them on the map
- Places API to retrieve additional details about a place on the map
- Suggest API to show suggestions while the user types a category
An interactive map is rendered using MapGL JS API.
The example contains two variables for API keys: API_KEY for search APIs and MAPGL_KEY for map APIs. If your key setup is different (one key for all services or separate keys per API), adjust the usage of variables accordingly.
Key blocks
Map initialization
Creating a MapGL JS API map instance with basic rendering parameters and an access key for Map Tiles API. Additionally, a map click handler is registered to start the main search flow.
function initMap() {
map = new mapgl.Map('map', {
center: DEFAULT_CENTER, // Map center coordinates (Moscow)
zoom: DEFAULT_ZOOM, // Initial zoom level
key: MAPGL_KEY, // Access key for Map Tiles API
enableTrackResize: true,
zoomControl: 'centerRight',
});
// Click on map to explore the area
map.on('click', handleMapClick);
}
After the user clicks the map, a circle (search radius) is drawn around the click point.
function drawSearchRadius(lngLat) {
// Remove the previously created circle
if (searchCircle) {
searchCircle.destroy();
}
searchCircle = new mapgl.Circle(map, {
coordinates: lngLat, // Circle center: click coordinates
radius: searchRadius, // Circle radius: search radius
color: '#0066cc22',
strokeColor: '#0066cc',
strokeWidth: 2,
interactive: false,
});
}
Reverse geocoding
Building a request to Geocoder API with the required parameters and an access key.
async function reverseGeocode(lngLat) {
const [lon, lat] = lngLat;
const url = new URL('https://catalog.api.2gis.com/3.0/items/geocode');
url.searchParams.set('lat', lat);
url.searchParams.set('lon', lon);
url.searchParams.set('key', API_KEY); // Access key for Geocoder API
url.searchParams.set('fields', 'items.point,items.address'); // Return coordinates and address in the response
url.searchParams.set('locale', 'en_RU');
try {
const response = await fetch(url);
const data = await response.json();
const result = checkApiResponse(data, 'Geocoder API');
if (result?.items?.length) {
const item = result.items[0];
const address =
item.full_name || item.address_name || item.name || 'Address not found';
addressDisplay.textContent = address;
} else {
addressDisplay.textContent = 'Address not found';
}
} catch (error) {
console.error('Geocoder API error:', error);
showError(`Geocoding error: ${error.message}`);
addressDisplay.textContent = 'Failed to get address';
}
}
The Geocoder API request is executed on every map click, inside the handleMapClick() handler.
async function handleMapClick(e) {
const { lngLat } = e; // Map click coordinates
currentLocation = lngLat;
clearMarkers();
drawSearchRadius(lngLat);
// 1. Geocoder API - get address for location
await reverseGeocode(lngLat);
if (selectedCategory) {
await loadNearbyMarkers(lngLat, selectedCategory);
}
}
The address from the response is displayed in the map widget.
Searching for places and displaying them on the map
Building a request to Markers API with search parameters and an access key. The number of results in the response is limited to 10.
async function loadNearbyMarkers(lngLat, query) {
if (!query || !query.trim()) {
showError('No category selected');
return;
}
const [lon, lat] = lngLat;
const url = new URL('https://catalog.api.2gis.com/3.0/markers');
url.searchParams.set('q', query); // Query string (place category)
url.searchParams.set('point', `${lon},${lat}`); // Search center (map click coordinates)
url.searchParams.set('radius', searchRadius); // Search radius in meters
url.searchParams.set('sort', 'distance'); // Sort results by distance from the search center
url.searchParams.set('key', API_KEY); // Access key for Markers API
url.searchParams.set('locale', 'en_RU');
url.searchParams.set('limit', 10); // Up to 10 results in response
try {
const response = await fetch(url);
const data = await response.json();
const result = checkApiResponse(data, 'Markers API');
if (result?.items?.length) {
renderMarkers(result.items);
updateStats(result.items.length, result.total);
} else {
updateStats(0, 0);
showError(`Nothing found within ${searchRadius}m`);
}
} catch (error) {
console.error('Markers API error:', error);
showError(`Failed to load markers: ${error.message}`);
updateStats(0, 0);
}
}
The Markers API request is executed in the following cases:
-
Inside
handleMapClick()- after a map click, if a category is already selected:// 2. Markers API - load nearby places if category is selected
if (selectedCategory) {
await loadNearbyMarkers(lngLat, selectedCategory);
} -
Inside
applyFilter()- when the user confirms a category (selects it from suggestions or presses Enter) and a map point has already been selected:function applyFilter(category) {
selectedCategory = category;
if (currentLocation) {
loadNearbyMarkers(currentLocation, category);
}
} -
Inside the radius slider handler
radiusSlider.addEventListener()- when the search radius changes and both the point and category are already set:if (currentLocation) {
drawSearchRadius(currentLocation);
if (selectedCategory) {
loadNearbyMarkers(currentLocation, selectedCategory);
}
}
The returned items are rendered on the map using mapgl.Marker. Each marker gets a click handler that triggers a Places API request to fetch detailed place information.
const marker = new mapgl.Marker(map, {
coordinates: [item.lon, item.lat],
});
marker.on('click', () => {
fetchPlaceDetails(item.id, [item.lon, item.lat]);
});
Place details
Building a request to Places API with the place ID, required response fields, and an access key.
async function fetchPlaceDetails(placeId, coordinates) {
// Show loading state
showLoadingPopup(coordinates);
const url = new URL('https://catalog.api.2gis.com/3.0/items/byid');
url.searchParams.set('id', placeId); // Place ID from Markers API
url.searchParams.set('key', API_KEY); // Access key for Places API
url.searchParams.set(
'fields',
'items.point,items.address,items.rubrics,items.schedule',
); // Which place data to include in the response
url.searchParams.set('locale', 'en_RU');
try {
const response = await fetch(url);
const data = await response.json();
const result = checkApiResponse(data, 'Places API');
if (result?.items?.length) {
const item = result.items[0];
showPlacePopup(item, coordinates);
} else {
showError('Place information not found');
closePopup();
}
} catch (error) {
console.error('Places API error:', error);
showError(`Failed to load details: ${error.message}`);
closePopup();
}
}
The request is sent when the user clicks one of the markers that were rendered after retrieving data from Markers API.
marker.on('click', () => {
fetchPlaceDetails(item.id, [item.lon, item.lat]);
});
The returned data is used in showPlacePopup() to display place information in a map popup using mapgl.HtmlMarker.
function formatTodayWorkingHours(schedule) {
if (!schedule) return '';
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const todayKey = days[new Date().getDay()];
const today = schedule[todayKey];
if (!today) return '';
const ranges = (today.working_hours || [])
.map((h) => {
if (!h) return '';
const from = h.from || '';
const to = h.to || '';
if (from && to) return `${from}–${to}`;
return from || to || '';
})
.filter(Boolean);
if (ranges.length) {
return `Hours today: ${ranges.join(', ')}`;
}
if (today.day_off || today.is_day_off) {
return 'Hours today: closed';
}
return '';
}
function showPlacePopup(item, coordinates) {
closePopup();
const name = item.name || 'Unnamed';
const address = item.address_name || item.address?.name || item.full_address_name || '';
const category = item.rubrics?.[0]?.name || '';
const workingHoursToday = formatTodayWorkingHours(item.schedule);
const content = `
<div class="popup">
<div class="popup__title">${escapeHtml(name)}</div>
${address ? `<div class="popup__address">${escapeHtml(address)}</div>` : ''}
${workingHoursToday ? `<div class="popup__address">${escapeHtml(workingHoursToday)}</div>` : ''}
${category ? `<div class="popup__category">${escapeHtml(category)}</div>` : ''}
</div>
`;
currentPopup = new mapgl.HtmlMarker(map, {
coordinates,
html: content,
anchor: [0.5, 1],
});
}
Category suggestions while typing
Building a request to Suggest API with the query text, location, suggestion type, and an access key.
async function fetchSuggestions(query) {
if (!query.trim()) {
hideSuggestions();
return;
}
// Use current location or map center
const [lon, lat] = currentLocation || map.getCenter();
const url = new URL('https://catalog.api.2gis.com/3.0/suggests');
url.searchParams.set('q', query); // User input text
url.searchParams.set('key', API_KEY); // Access key for Suggest API
url.searchParams.set('location', `${lon},${lat}`); // Current location or map center
url.searchParams.set('locale', 'en_RU');
url.searchParams.set('type', 'rubric'); // Suggest only categories
try {
const response = await fetch(url);
const data = await response.json();
const result = checkApiResponse(data, 'Suggest API');
if (result?.items?.length) {
renderSuggestions(result.items);
} else {
hideSuggestions();
}
} catch (error) {
console.error('Suggest API error:', error);
showError(`Suggestions error: ${error.message}`);
hideSuggestions();
}
}
The request is sent when the user starts typing in the category search field. A 300 ms delay is used to avoid flooding the API during fast typing.
const handleInput = debounce((e) => {
fetchSuggestions(e.target.value);
}, 300);
searchInput.addEventListener('input', handleInput);
The returned suggestions are displayed as a dropdown list. When the user clicks a suggestion, its name is set in the input, the list is hidden, and a marker search is triggered for the selected category.
suggestionsContainer.querySelectorAll('.suggestion-item').forEach((el) => {
el.addEventListener('click', () => {
const name = el.dataset.name;
searchInput.value = name;
hideSuggestions();
applyFilter(name);
});
});