Сгруппируйте данные по координатам:
const grouped = productData.reduce((acc, n) => (
(acc[n.coordinates] ??= []).push(n),
acc
), {});
Соответственно, когда будете собирать строку для ballonContent метки, вместо одного объекта придётся пробежать по массиву объектов:
for (const [ coord, data ] of Object.entries(grouped)) {
const placemark = new ymaps.Placemark(
coord.split(',').map(parseFloat),
{
balloonContent: data
.map(n => `
<div>
${n.address}
<br>
<a href="${n.productURL}">Подробнее</a>
</div>`)
.join(''),
},
{
preset: 'islands#blueDotIcon',
maxWidth: 300,
}
);
map.geoObjects.add(placemark);
}
Или, воспользуйтесь
кластеризатором:
const placemarks = productData.map((n, i) => new ymaps.Placemark(
n.coordinates.split(',').map(parseFloat),
{
balloonContent: `${n.address}<br><a href="${n.productURL}">Подробнее</a>`,
clusterCaption: `Адрес №${i + 1}`,
},
{
preset: 'islands#blueDotIcon',
maxWidth: 300,
}
));
const clusterer = new ymaps.Clusterer({
clusterDisableClickZoom: true,
});
clusterer.add(placemarks);
map.geoObjects.add(clusterer);