Export a Google Maps list to CSV, Excel or KML, free (saved or shared lists)

Download any Google Maps list as a spreadsheet or a KML file: name, address, coordinates, your notes and a link for every place. Works with your own saved lists and with lists someone shared with you. Free, in your own browser, without installing anything or connecting your Google account to any site.

Open the list in a computer browser (Saved > Lists, or the link someone shared with you), reload the page once, then run the script.

export-google-maps-list.js · 144 linespaste in the DevTools console on www.google.com
// Responsive Muse · Google Maps List Exporter · v1.0 · 2026-11-05
// Saves a Google Maps list (one of your saved lists or any list someone shared with you) to CSV,
// KML or plain text: name, address, latitude/longitude, your note, date added and a Maps link for
// every place. Open the list on Google Maps in a computer browser and run the script.
// It reads the list the page already loaded: it makes no network requests of its own and never
// reads cookies, passwords or tokens.
(async () => {
  const TOOL = 'Export Google Maps list';
  const FORMAT = 'csv';        // 'csv' (Excel, Sheets), 'kml' (Google My Maps, Google Earth) or 'txt' (one place per line)

  if (!/^(www\.)?google\.[a-z.]+$/.test(location.hostname) || !location.pathname.startsWith('/maps')) {
    alert('Open the list on Google Maps first (Saved > Lists, or the link someone shared with you):\nhttps://www.google.com/maps');
    return;
  }
  // A small box on the page shows progress and asks before changing anything,
  // so you never need to read or edit the console.
  const many = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;   // 1 video, 2 videos
  const panel = (() => {
    const box = document.createElement('div');
    box.style.cssText = 'display:block!important;position:fixed;right:16px;bottom:16px;z-index:2147483647;width:340px;max-width:calc(100vw - 32px);' +
      'background:#111318;color:#e6e8ee;font:14px/1.5 system-ui,sans-serif;border-radius:12px;padding:16px;' +
      'box-shadow:0 8px 30px rgba(0,0,0,.4);text-align:left';
    const head = document.createElement('strong');
    head.textContent = TOOL;
    const msg = document.createElement('p');
    msg.style.cssText = 'margin:8px 0 12px;white-space:pre-line';
    const row = document.createElement('div');
    row.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end';
    box.append(head, msg, row);
    document.body.append(box);
    const button = (label, primary, onClick) => {
      const b = document.createElement('button');
      b.textContent = label;
      b.style.cssText = 'font:600 14px system-ui,sans-serif;padding:8px 14px;border-radius:8px;cursor:pointer;' +
        (primary ? 'background:#0f766e;color:#fff;border:1px solid #0f766e' : 'background:none;color:#e6e8ee;border:1px solid #3a3f4b');
      b.onclick = onClick;
      row.append(b);
    };
    return {
      text(t) { msg.textContent = t; },
      ask(t, yes) {
        msg.textContent = t;
        row.replaceChildren();
        return new Promise(r => {
          button('Cancel', false, () => { row.replaceChildren(); r(false); });
          button(yes, true, () => { row.replaceChildren(); r(true); });
        });
      },
      stoppable(onStop) { row.replaceChildren(); button('Stop', false, onStop); },
      actions(t, list) {
        msg.textContent = t;
        row.replaceChildren();
        list.forEach(([label, primary, run]) => button(label, primary, run));
      },
      finish(t) { this.actions(t, [['Close', true, () => box.remove()]]); },
      close() { box.remove(); },
    };
  })();
  // Offer the result as a downloaded file or as clipboard text (a real click, so it always works)
  const deliver = (summary, body, file) => {
    const save = () => {
      const a = document.createElement('a');
      a.href = URL.createObjectURL(new Blob([/\.csv$/.test(file) ? '\ufeff' + body : body], { type: 'text/plain;charset=utf-8' }));
      a.download = file;
      a.click();
      setTimeout(() => URL.revokeObjectURL(a.href), 5000);
      panel.text(`${summary}\nSaved as ${file} (check your Downloads).`);
    };
    const copy = () => navigator.clipboard.writeText(body)
      .then(() => panel.text(`${summary}\nCopied. Paste it into a spreadsheet or a note.`))
      .catch(() => panel.text(`${summary}\nThe browser blocked the clipboard: use Download instead.`));
    panel.actions(summary, [['Close', false, () => panel.close()], ['Copy', false, copy], ['Download', true, save]]);
  };

  // 1. The whole list comes with the page itself, inside its start-up data
  let raw = '';
  const seek = v => {
    if (raw) return;
    if (typeof v === 'string') {
      if (v.startsWith(")]}'") && v.includes('placelists/list')) raw = v;
    } else if (v && typeof v === 'object') {
      for (const k in v) seek(v[k]);
    }
  };
  seek(window.APP_INITIALIZATION_STATE);
  let info = null;
  try { info = JSON.parse(raw.slice(raw.indexOf('\n') + 1))[0]; } catch (e) { /* handled below */ }
  const items = Array.isArray(info?.[8]) ? info[8] : [];
  const title = typeof info?.[4] === 'string' ? info[4] : '';
  // Opening a list from the menu changes the page without reloading it, so the start-up data
  // may belong to another list (or to none): then a reload fixes it.
  const shown = document.title.replace(/\s*[-–]\s*Google Maps\s*$/, '').trim();
  if (!items.length || !title || (shown && shown !== 'Google Maps' && shown !== title)) {
    panel.finish('Reload this page (F5) with the list open and run the script again.\n' +
      'If it still finds nothing, open the list itself (not the Saved menu) and make sure it has places.');
    return 0;
  }

  // 2. One row per place
  const day = t => t ? new Date(t * 1000).toISOString().slice(0, 10) : '';
  const places = items.map(e => {
    const p = Array.isArray(e?.[1]) ? e[1] : [];
    const at = Array.isArray(p[5]) ? p[5] : [];
    const cid = Array.isArray(p[6]) && p[6][1] ? BigInt.asUintN(64, BigInt(p[6][1])).toString() : '';
    const name = String(e?.[2] || (p[2] || '').split(',')[0] || '').trim();
    return {
      name,
      address: String(p[4] || '').trim(),
      lat: typeof at[2] === 'number' ? at[2] : '',
      lng: typeof at[3] === 'number' ? at[3] : '',
      note: String(e?.[3] || '').trim(),
      added: day(Array.isArray(e?.[9]) && typeof e[9][0] === 'number' ? e[9][0] : 0),
      url: cid ? `https://maps.google.com/?cid=${cid}`
        : name ? `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(name + ' ' + (p[4] || ''))}` : '',
    };
  }).filter(x => x.name);

  // 3. Build the file and offer it
  const cell = v => '"' + String(v).replace(/"/g, '""') + '"';
  const xml = v => String(v).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  let body;
  if (FORMAT === 'kml') {
    body = ['<?xml version="1.0" encoding="UTF-8"?>', '<kml xmlns="http://www.opengis.net/kml/2.2"><Document>',
      `<name>${xml(title)}</name>`,
      ...places.filter(x => x.lat !== '').map(x => '<Placemark>' +
        `<name>${xml(x.name)}</name>` +
        `<description>${xml([x.address, x.note, x.url].filter(Boolean).join('\n'))}</description>` +
        `<Point><coordinates>${x.lng},${x.lat},0</coordinates></Point></Placemark>`),
      '</Document></kml>'].join('\n');
  } else if (FORMAT === 'txt') {
    body = places.map(x => [x.name, x.address, x.note].filter(Boolean).join(' - ')).join('\n');
  } else {
    body = ['Name,Address,Latitude,Longitude,Note,Date added,Google Maps URL',
      ...places.map(x => [x.name, x.address, x.lat, x.lng, x.note, x.added, x.url].map(cell).join(','))].join('\n');
  }
  const total = typeof info[12] === 'number' ? info[12] : places.length;
  const notes = places.filter(x => x.note).length;
  const summary = `"${title}": ${many(places.length, 'place')}` +
    (total > places.length ? ` of ${total} (Google only sent these with the page)` : '') +
    `, ${many(notes, 'note')}.`;
  deliver(summary, body, `google-maps-list-${title.replace(/[\\/:*?"<>|\s]+/g, '_').slice(0, 60) || 'list'}.${FORMAT}`);
  return places.length;
})();

🛡️ Chrome says “Don't paste code…”? That's normal. Type allow pasting in the console, press Enter and paste again. You only need to do it once.

Why does Chrome warn me?

Chrome shows this the first time anyone pastes into the console, to protect people from scams that ask them to paste unknown code. It's right to warn you, and that's why this script is fully visible above: 144 commented lines that send nothing anywhere and never touch your passwords or cookies. Why it's safe.

Rather not use the console? Drag the Export Google Maps list button to your bookmarks bar and click it on www.google.com. No warning that way.

Bookmarklet: drag the middle button to your bookmarks bar, open www.google.com and click the bookmark. It runs the same script with the options above.

Why it's safe to runAutomated check passed Nov 9, 2026 · tested on a real account Nov 5, 2026
  • Sends nothing anywhere. No fetch, XHR or external scripts. The only network activity is www.google.com itself loading your list.
  • Never touches your login. It doesn't read cookies, passwords, tokens or browser storage.
  • Only runs on www.google.com. Anywhere else it stops before doing anything.
  • Short and readable. 144 commented lines, not minified or obfuscated.
  • Shows its progress on the page. A small box tells you what it's doing and offers Download or Copy at the end.
  • Same code for everyone. SHA-256 of the default version: 1490c7eec18dfb0c…
Verify it yourself in 30 seconds
  1. Read it: every line is above, with comments.
  2. Search the code for fetch, XMLHttpRequest, cookie, localStorage or eval. None of them appear.
  3. Open the Network tab before running it: you'll only see requests to www.google.com.
Did it work for you?

How to use it

  1. Open the list on Google Maps in a computer browser: Saved > Lists for yours, or the link someone sent you. Then reload the page once (F5).
  2. Open the console: F12 or Ctrl+Shift+J (Cmd+Option+J on Mac). If Chrome asks, type allow pasting and press Enter.
  3. Copy the script, paste it in the console and press Enter. It reads the list the page already loaded and offers the file straight away.

What you get

One row per place: name, address, latitude, longitude, the note saved with it, the date it was added and a Google Maps link that opens that exact place. Choose KML to import the list into Google My Maps, Google Earth or another map app, or TXT for a plain list.

Why not Google Takeout?

Takeout only exports lists you created, takes a while to arrive and gives you links instead of addresses and coordinates. Other tools are Python scripts, paid scrapers or extensions that ask for access to your account. This one is short, readable and runs only on Google Maps, in your own tab.

FAQ

Does it work with lists someone shared with me?

Yes. Any list you can open on Google Maps works: your own lists and lists shared with you by link. Google Takeout only exports the lists you created yourself.

What is in the file?

One row per place: name, address, latitude and longitude, your note, the date it was added and a Google Maps link that opens that exact place.

How do I move the list to Apple Maps, Waze or another app?

Download the KML: Google My Maps, Google Earth and most map apps can import it. For a spreadsheet, choose CSV.

It says "Reload this page". Why?

When you open a list from the Saved menu, Google Maps changes the view without loading the page again, so the list data is not there yet. Press F5 with the list open and run the script again.

Do I need to be signed in?

Only for your private lists. Shared lists open for anyone with the link.

More Google Maps tools

Export & back up