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