Skip to main content

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:

An interactive map is rendered using MapGL JS API.

Access keys

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);
});
});