Greasy Fork is available in English.

Guess retriever

Retrieves guesses under a certain score threshold from your duels

// ==UserScript==
// @name         Guess retriever
// @version      0.2
// @description  Retrieves guesses under a certain score threshold from your duels
// @author       You
// @license      MIT
// @match        https://www.geoguessr.com/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        unsafeWindow
// @require      https://unpkg.com/@popperjs/core@2.11.5/dist/umd/popper.min.js
// @namespace    https://greasyfork.org/users/1011193
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @require      https://greasyfork.org/scripts/460322-geoguessr-styles-scan/code/Geoguessr%20Styles%20Scan.js?version=1151668
// ==/UserScript==


let hide = false;
let styles = GM_getValue("guessFinderStyles");
if (!styles) {
    hide = true;
    styles = {};
}
let css =`
#guessFinderPopupWrapper, #guessFinderSearchWrapper, #guessFinderSlantedRoot, #guessFinderSlantedStart, #guessFinderInputWrapper, #guessFinderPopup, #guessFinderToggle, #guessFinderTogglePicture, #runButton, .buttonContainer {
    box-sizing: border-box;
  }

  #guessFinderPopup {
    background: rgba(26, 26, 46, 0.9);
    padding: 15px;
    border-radius: 10px;
    max-height: 80vh;
    overflow-y: auto;
    width: 28em;
  }

  #guessFinderPopup div {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 10px;
  }

  #guessFinderPopup input {
    background: rgba(255,255,255,0.1);
    color: white;
    border: none;
    border-radius: 5px;
  }

  .buttonContainer {
    display: flex;
    justify-content: center;
    align-items: center;
    margin-top: 20px;
  }

  #runButton {
    background: rgba(255, 255, 255, 0.1);
    color: white;
    border: none;
    border-radius: 5px;
    padding: 10px 20px;
  }

  #runButton:hover {
    background: rgba(255, 255, 255, 0.25);
    cursor: pointer;
  }

  #guessFinderToggle {
    width: 59.19px;
  }

  #guessFinderTogglePicture {
    justify-content: center;
  }

  #guessFinderTogglePicture img {
    width: 20px;
    filter: brightness(0) invert(1);
    opacity: 60%;
  }

.inputContainer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 10px;
}

.inputLabel {
  margin: 0;
  padding-right: 6px;
  color: white; /* Adjust if necessary */
}

#dropdownInput {
  background: rgba(255,255,255,0.1);
  color: white;
  border: none;
  border-radius: 5px;
  padding: 5px; /* Adjust padding as needed */
}
  `

GM_addStyle(css);

const guiHTMLHeader = `
<div id="guessFinderPopupWrapper">
  <div id="guessFinderSearchWrapper">
    <div id="guessFinderSlantedRoot">
      <div id="guessFinderSlantedStart"></div>
      <div id="guessFinderInputWrapper">
        <div id="guessFinderPopup" style="background: rgba(26, 26, 46, 0.9); padding: 15px; border-radius: 10px; max-height: 80vh; overflow-y: auto; width: 28em">
          <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 10px;">
            <span id="mmaAPIkey" style="margin: 0; padding-right: 6px;"> Map Making App API</span>
            <input id="mmaAPIkeyInput" name="mmaAPIkey" style="background: rgba(255,255,255,0.1); color: white; border: none; border-radius: 5px">
          </div>
          <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 10px;">
            <span id="scoreThresholdLabel" style="margin: 0; padding-right: 6px;">Score Threshold</span>
            <input type="number" id="scoreThresholdInput" name="scoreThreshold" style="background: rgba(255,255,255,0.1); color: white; border: none; border-radius: 5px">
          </div>

          <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 10px;">
            <span id="monthsOfDuelsLabel" style="margin: 0; padding-right: 6px;">Months of Duels</span>
            <input type="number" id="monthsOfDuelsInput" name="monthsOfDuels" style="background: rgba(255,255,255,0.1); color: white; border: none; border-radius: 5px">
          </div>
          <div class="inputContainer">
              <span class="inputLabel">Map Making App Map:</span>
              <select id="dropdownInput" name="dropdownInput">
                   <option value="option1">Option 1</option>
                   <option value="option2">Option 2</option>
                   <option value="option3">Option 3</option>
                   <option value="option4">Option 4</option>
              </select>
          </div>
          <div style="display: flex; justify-content: center; align-items: center; margin-top: 20px;">
            <button id="runButton" >Run</button>
          </div>
          <div style="display: flex; justify-content: center; align-items: center; margin-top: 20px;">
            <p id="guessFinderStatus"></p>
          </div>
        </div>
        <button style="width: 59.19px" id="guessFinderToggle"><picture id="guessFinderTogglePicture" style="justify-content: center"><img src="https://www.svgrepo.com/show/532540/location-pin-alt-1.svg" style="width: 20px; filter: brightness(0) invert(1); opacity: 60%;"></picture></button>
      </div>
      <div id="guessFinderSlantedEnd"></div>
    </div>
  </div>
</div>

`

const showPopup = (showButton, popup) => {
    popup.style.display = 'block';
    Popper.createPopper(showButton, popup, {
        placement: 'bottom',
        modifiers: [
            {
                name: 'offset',
                options: {
                    offset: [0, 10],
                },
            },
        ],
    });
}

const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

const iterativeSetTimeout = async (func, initDelay, cond) => {
    while (!cond()) {
        await delay(initDelay);
        await func();
        initDelay *= 2;
    }
};


const stylesUsed = [
    "header_item__",
    "quick-search_wrapper__",
    "slanted-wrapper_root__",
    "slanted-wrapper_variantGrayTransparent__",
    "slanted-wrapper_start__",
    "quick-search_searchInputWrapper__",
    "slanted-wrapper_end__",
    "slanted-wrapper_right__",
    "quick-search_searchInputButton__",
    "quick-search_iconSection__",
];

const uploadDownloadStyles = async () => {
    stylesUsed.forEach(style => {
    });
    await iterativeSetTimeout(scanStyles, 0.1, () => checkAllStylesFound(stylesUsed) !== undefined);
    if (hide) {
        document.querySelector("#guessFinderPopupWrapper").hidden = "";
    }
    stylesUsed.forEach(style => {
        styles[style] = cn(style);
    });
    setStyles();
    GM_setValue("guessFinderStyles", styles);
}

const getStyle = style => {
    return styles[style];
}

const setStyles = () => {
    try {
        document.querySelector("#guessFinderPopupWrapper").className = getStyle("header_item__");
        document.querySelector("#guessFinderSearchWrapper").className = getStyle("quick-search_wrapper__");
        document.querySelector("#guessFinderSlantedRoot").className = getStyle("slanted-wrapper_root__") + " " + getStyle("slanted-wrapper_variantGrayTransparent__");
        document.querySelector("#guessFinderSlantedStart").className = getStyle("slanted-wrapper_start__")+ " " + getStyle("slanted-wrapper_right__");
        document.querySelector("#guessFinderInputWrapper").className = getStyle("quick-search_searchInputWrapper__");
        document.querySelector("#guessFinderSlantedEnd").className = getStyle("slanted-wrapper_end__")+ " " + getStyle("slanted-wrapper_right__");
        document.querySelector("#guessFinderToggle").className = getStyle("quick-search_searchInputButton__");
        document.querySelector("#guessFinderLabel1").className = getStyle("label_sizeXSmall__") + getStyle("label_variantWhite__");
        document.querySelector("#guessFinderLabel2").className = getStyle("label_sizeXSmall__") + getStyle("label_variantWhite__");
        document.querySelector("#guessFinderTogglePicture").className = getStyle("quick-search_iconSection__");
        document.querySelectorAll(".deleteButton").forEach(el => el.className = el.className + " " + getStyle("quick-search_searchInputButton__"));
    } catch (err) {
        console.error(err);
    }
}


const insertHeaderGui = async (header, gui) => {

    header.insertAdjacentHTML('afterbegin', gui);

    // Resolve class names
    if (hide) {
        document.querySelector("#guessFinderPopupWrapper").hidden = "true"
    }

    scanStyles().then(() => uploadDownloadStyles());
    setStyles();

    const showButton = document.querySelector('#guessFinderToggle');
    const popup = document.querySelector('#guessFinderPopup');
    popup.style.display = 'none';

    document.addEventListener('click', (e) => {
        const target = e.target;
        if (target == popup || popup.contains(target) || !document.contains(target)) return;
        if (target.matches('#guessFinderToggle, #guessFinderToggle *')) {
            e.preventDefault();
            showPopup(showButton, popup);
        } else {
            popup.style.display = 'none';
        }
    });
}

let MAP_MAKING_API_KEY = localStorage.getItem("guessFinderMMAApiKey");

async function mmaFetch(url, options = {}) {
	const response = await fetch(new URL(url, 'https://map-making.app'), {
		...options,
		headers: {
			accept: 'application/json',
			authorization: `API ${MAP_MAKING_API_KEY.trim()}`,
			...options.headers
		}
	});
	if (!response.ok) {
		let message = 'Unknown error';
		try {
			const res = await response.json();
			if (res.message) {
				message = res.message;
			}
		} catch {}
		alert(`An error occurred while trying to connect to Map Making App. ${message}`);
		throw Object.assign(new Error(message), { response });
	}
	return response;
}

async function getMaps(suppress = false) {
    if (MAP_MAKING_API_KEY === null) {
        if (!suppress) alert("Please input an API key for Map Making App");
        return;
    }
	const response = await mmaFetch(`/api/maps`);
	const maps = await response.json();
	return maps;
}

async function importLocations(mapId, locations) {
	const response = await mmaFetch(`/api/maps/${mapId}/locations`, {
		method: 'post',
		headers: {
			'content-type': 'application/json'
		},
		body: JSON.stringify({
			edits: [{
				action: { type: 4 },
				create: locations,
				remove: []
			}]
		})
	});
	await response.json();
}

const API_Key = 'bdc_e4a84278a5684f4786dd8277e4948ac4';

const ERROR_RESP = -1000000;

let count = 0;

async function getCountryCode(coords) {
    if (coords[0] <= -85.05) return 'AQ';
    count++
    if (API_Key.toLowerCase().match("^(bdc_)?[a-f0-9]{32}$") != null) {
        const api = "https://api.bigdatacloud.net/data/reverse-geocode?latitude="+coords.lat+"&longitude="+coords.lng+"&localityLanguage=en&key="+API_Key;
        return await fetch(api)
            .then(res => (res.status !== 200) ? ERROR_RESP : res.json())
            .then(out => (out === ERROR_RESP) ? ERROR_RESP : CountryDict[out.countryCode]);
    } else {
        const api = `https://nominatim.openstreetmap.org/reverse.php?lat=${coords.lat}&lon=${coords.lng}&zoom=21&format=jsonv2&accept-language=en`;
        return await fetch(api)
            .then(res => (res.status !== 200) ? ERROR_RESP : res.json())
            .then(out => (out === ERROR_RESP) ? ERROR_RESP : CountryDict[out?.address?.country_code?.toUpperCase()]);
    }
};

const fetchGeoGuessrData = async (id, cutoffDate) => {
    let after = null;
    let paginationToken = "", fetchedGames = [];
    let page = 0;
    let duelsFound = 0;
    while (true) {
        page++
        let url = "https://www.geoguessr.com/api/v4/feed/private";
        if (paginationToken !== "") {
            url += "?paginationToken=" + paginationToken;
        }
        let response = await fetch(url),
            n = await response.text(),
            jsonData = JSON.parse(n);
        if (jsonData.entries.length === 0) {
            console.log("All data fetched.");
            break;
        }
        const filteredEntries = jsonData.entries.filter(entry => {
            const entryTime = new Date(entry.time);
            const cutoffTime = new Date(cutoffDate);
            return entryTime >= cutoffTime;
        });
        if (filteredEntries.length === 0) {
            console.log("All data fetched.");
            break;
        }

        // Extract game IDs from filtered entries
        for (const entry of filteredEntries) {
            // Parse the payload as JSON
            const payloadData = JSON.parse(entry.payload);
            if (Array.isArray(payloadData)) {
                // If payloadData is an array, process each item
                for (const payloadItem of payloadData) {
                    if (payloadItem.payload && payloadItem.payload.gameId && payloadItem.payload.gameMode === "Duels") {
                        fetchedGames.push(payloadItem.payload.gameId);
                        duelsFound++;
                    }
                }
            } else {
                // If payloadData is an object, check and process directly
                if (payloadData.gameId && payloadData.gameMode === "Duels") {
                    fetchedGames.push(payloadData.gameId);
                }
            }
        }

        statusUpdater.updateStatus("fetchingDuels", duelsFound, new Date(filteredEntries[filteredEntries.length-1].time).toISOString());
        paginationToken = jsonData.paginationToken;
        await new Promise(resolve => {
            setTimeout(() => {
                resolve();
            }, 500);
        });
    }
    statusUpdater.updateStatus("filteringDuplicates");
    fetchedGames = fetchedGames.filter((game, index, array) => array.indexOf(game) === index); // removes duplicates
//    return fetchedGames.includes(after) ? fetchedGames.slice(0, fetchedGames.indexOf(after)) : fetchedGames;
    return fetchedGames;
};

const findIncorrectGuesses = async (gameData, playerId, status, score = null) => {
    let incorrectRounds = [];

    const rounds = gameData.rounds;

    // Finding the team and player by playerId
    let foundPlayer = null;
    let t = null;
    gameData.teams.forEach(team => {
        team.players.forEach(player => {
            if (player.playerId == playerId) {
                foundPlayer = player;
                t = team;
            }
        });
    });

    if (!foundPlayer) {
        return []; // Player not found, returning empty array
    }

     for (const round of t.roundResults) {
         let guessedCountryCode = null, actualCountryCode = null
         const roundLocation = rounds.find(r => r.roundNumber === round.roundNumber).panorama;
         status.guesses++;
         console.log(round);
         if (score === null) {
             return;
             guessedCountryCode = await getCountryCode({ lat: round.bestGuess.lat, lng: round.bestGuess.lng });
             actualCountryCode = await getCountryCode({ lat: roundLocation.lat, lng: roundLocation.lng });
             if (guessedCountryCode !== actualCountryCode) {
                 status.matching++;
                 incorrectRounds.push({
                     roundNumber: round.roundNumber,
                     score: round.score,
                     guessedCountryCode,
                     actualCountryCode,
                     panorama: roundLocation
                 });
             }
         } else if (round.score <= score) {
             status.matching++;
             incorrectRounds.push({
                 roundNumber: round.roundNumber,
                 score: round.score,
                 guessedCountryCode,
                 actualCountryCode,
                 panorama: roundLocation
             });
         }
         statusUpdater.updateStatus('findingGuesses', status.guesses, score === null ? 'wrongCountry' : "belowThreshold", status.matching);
    }
    return incorrectRounds;
};

const decodePanoId = (hexString) => {
    let decodedString = "";
    for (let i = 0; i < hexString.length; i += 2) {
        const hexPair = hexString.substr(i, 2);
        const char = String.fromCharCode(parseInt(hexPair, 16));
        decodedString += char;
    }
    return decodedString;
};

function extractDate(timestamp) {
    var match = timestamp.match(/^(\d{4}-\d{2})/);
    return match ? match[1] : null;
}

const formatScore = score => {
    const base = Math.floor(score / 1000);
    const decimal = Math.floor((score % 1000) / 100) * 0.1;
    const formattedScore = base + decimal;

    return formattedScore.toFixed(1) + 'k';
};

const subtractMonths = (months) => {
    if (months < 0) {
        throw new Error('Number of months must be positive');
    }

    const date = new Date();
    date.setMonth(date.getMonth() - months);

    return date.toISOString().split('T')[0]; // Returns the date in 'YYYY-MM-DD' format
};

function StatusUpdater(elementId) {
  this.element = document.getElementById(elementId);
  this.states = {
    fetchingDuels: (duelsCount, currentDate) =>
      `Fetching duel ids. <br/> Duels found: ${duelsCount} <br/> Checked through date: ${currentDate}`,
    filteringDuplicates: () => `Filtering out duplicate ids`,
    findingGuesses: (guessesCount, additionalState, additionalCount) => {
        let additionalText = '';
        if (additionalState === 'wrongCountry') {
            additionalText = ` Finding wrong country guesses. Guesses found: ${additionalCount}`;
        } else if (additionalState === 'belowThreshold') {
            additionalText = ` Finding guesses below score threshold. Guesses found: ${additionalCount}`;
        }
        return `Finding guesses. <br/> Guesses checked: ${guessesCount} <br/> ${additionalText}`;
    },
    pushingLocations: (mapName, locationsCount) =>
      `Pushing locations to ${mapName} <br/> locations pushed: ${locationsCount}`,
    done: () => `Done!`
  };
}

StatusUpdater.prototype.updateStatus = function(state, ...args) {
  if (this.states[state]) {
    this.element.innerHTML = this.states[state](...args);
  } else {
    console.error("Invalid state");
  }
};

let statusUpdater;

let userId = localStorage.getItem("guessFinderUserId")
if (userId == null) {
    fetch('https://geoguessr.com/api/v3/profiles', {method: "GET", "credentials": "include"})
        .then(response => response.json())
        .then(data => {
        if(data && data.user.id) {
            userId = data.user.id;
            localStorage.setItem("guessFinderUserId", userId);
        } else {
            console.log('ID not found in the response');
        }
    })
        .catch(error => console.error('Error:', error));
}

const run = async () => {
    MAP_MAKING_API_KEY = document.getElementById('mmaAPIkeyInput').value
    localStorage.setItem('guessFinderMMAApiKey', MAP_MAKING_API_KEY);
    const maps = await getMaps();
    const mapId = document.getElementById('dropdownInput').value;
    if (mapId == "") return;
    const months = document.getElementById("monthsOfDuelsInput").value;
    if (months <= 0) return;
    const scoreThreshold = document.getElementById("scoreThresholdInput").value;
    if (scoreThreshold == "" || scoreThreshold < 0) return;
    const map = maps.find(map => map.id == mapId);
    const duelIds = await fetchGeoGuessrData(userId, subtractMonths(months));
    let locsPushed = 0;
    let status = {matching: 0, guesses: 0}
    for (const id of duelIds) {
        let api_url = `https://game-server.geoguessr.com/api/duels/${id}`;
        let res = await fetch(api_url, {method: "GET", "credentials": "include"})
        let json = await res.json();
        const incorrectGuesses = await findIncorrectGuesses(json, userId, status, scoreThreshold);
        if (incorrectGuesses.length) {
//            console.log(`https://www.geoguessr.com/duels/${res}/summary`);
  //          console.log(incorrectGuesses);
            for (const guess of incorrectGuesses) {
                let loc = guess.panorama;
                if (loc.panoId) loc.panoId = decodePanoId(loc.panoId);
                let tags = [];
                if (guess.guessedCountryCode) tags.push(`guessed ${guess.guessedCountryCode}`)
                if (guess.actualCountryCode) tags.push(`actual ${guess.actualCountryCode}`)
                tags.push(`date: ${extractDate(json.rounds[0].startTime)}`)
                tags.push(`score: ${formatScore(guess.score)}`)
                await importLocations(map.id, [{
                    id: -1,
                    location: loc,
                    panoId: loc.panoId ?? null,
                    heading: loc.heading ?? 0,
                    pitch: loc.pitch ?? 0,
                    zoom: loc.zoom === 0 ? null : loc.zoom,
                    tags,
                    flags: loc.panoId ? 1 : 0
                }]);
                locsPushed++;
            }
        }
    }
    statusUpdater.updateStatus("done");
}

const populateMaps = maps => {
    const dropdown = document.querySelector("#dropdownInput");
    maps.forEach( item => {
        let option = document.createElement('option');
        option.value = item.id;
        option.textContent = item.name;
        dropdown.appendChild(option);
    });
};

const addPopup = async (refresh=false) => {
    if (refresh || (document.querySelector('[class^=header_header__]') && document.querySelector('#guessFinderPopupWrapper') === null)) {
        if (!refresh) {
            insertHeaderGui(document.querySelector('[class^=header_context__]'), guiHTMLHeader)
            const dropdown = document.querySelector("#dropdownInput");
            const maps = await getMaps(true);

            // Clear existing options
            dropdown.innerHTML = '';
            // Append new options
            let defaultOption = document.createElement('option');
            defaultOption.value = '';
            defaultOption.textContent = ''; // Empty text for default option
            dropdown.appendChild(defaultOption);

            if (maps) {
                populateMaps(maps);
            }

            let apiKey = document.getElementById("mmaAPIkeyInput");
            apiKey.value = MAP_MAKING_API_KEY;
//            console.log(MAP_MAKING_API_KEY);

            apiKey.addEventListener("input", async () => {
                MAP_MAKING_API_KEY = apiKey.value
                const maps = await getMaps();
                populateMaps(maps);
            });

            const runButton = document.getElementById('runButton');
            if (runButton) {
                runButton.addEventListener('click', run);
            }
            statusUpdater = new StatusUpdater("guessFinderStatus");
        }
    }
}

const updateImage = (refresh=false) => {
    // Don't do anything while the page is loading
    if (document.querySelector("[class^=page-loading_loading__]")) return;
    addPopup();
}


new MutationObserver(async (mutations) => {
    updateImage()
}).observe(document.body, { subtree: true, childList: true });

const CountryDict = {
    AF: 'AF',
    AX: 'FI', // Aland Islands
    AL: 'AL',
    DZ: 'DZ',
    AS: 'US', // American Samoa
    AD: 'AD',
    AO: 'AO',
    AI: 'GB', // Anguilla
    AQ: 'AQ', // Antarctica
    AG: 'AG',
    AR: 'AR',
    AM: 'AM',
    AW: 'NL', // Aruba
    AU: 'AU',
    AT: 'AT',
    AZ: 'AZ',
    BS: 'BS',
    BH: 'BH',
    BD: 'BD',
    BB: 'BB',
    BY: 'BY',
    BE: 'BE',
    BZ: 'BZ',
    BJ: 'BJ',
    BM: 'GB', // Bermuda
    BT: 'BT',
    BO: 'BO',
    BQ: 'NL', // Bonaire, Sint Eustatius, Saba
    BA: 'BA',
    BW: 'BW',
    BV: 'NO', // Bouvet Island
    BR: 'BR',
    IO: 'GB', // British Indian Ocean Territory
    BN: 'BN',
    BG: 'BG',
    BF: 'BF',
    BI: 'BI',
    KH: 'KH',
    CM: 'CM',
    CA: 'CA',
    CV: 'CV',
    KY: 'UK', // Cayman Islands
    CF: 'CF',
    TD: 'TD',
    CL: 'CL',
    CN: 'CN',
    CX: 'AU', // Christmas Islands
    CC: 'AU', // Cocos (Keeling) Islands
    CO: 'CO',
    KM: 'KM',
    CG: 'CG',
    CD: 'CD',
    CK: 'NZ', // Cook Islands
    CR: 'CR',
    CI: 'CI',
    HR: 'HR',
    CU: 'CU',
    CW: 'NL', // Curacao
    CY: 'CY',
    CZ: 'CZ',
    DK: 'DK',
    DJ: 'DJ',
    DM: 'DM',
    DO: 'DO',
    EC: 'EC',
    EG: 'EG',
    SV: 'SV',
    GQ: 'GQ',
    ER: 'ER',
    EE: 'EE',
    ET: 'ET',
    FK: 'GB', // Falkland Islands
    FO: 'DK', // Faroe Islands
    FJ: 'FJ',
    FI: 'FI',
    FR: 'FR',
    GF: 'FR', // French Guiana
    PF: 'FR', // French Polynesia
    TF: 'FR', // French Southern Territories
    GA: 'GA',
    GM: 'GM',
    GE: 'GE',
    DE: 'DE',
    GH: 'GH',
    GI: 'UK', // Gibraltar
    GR: 'GR',
    GL: 'DK', // Greenland
    GD: 'GD',
    GP: 'FR', // Guadeloupe
    GU: 'US', // Guam
    GT: 'GT',
    GG: 'GB', // Guernsey
    GN: 'GN',
    GW: 'GW',
    GY: 'GY',
    HT: 'HT',
    HM: 'AU', // Heard Island and McDonald Islands
    VA: 'VA',
    HN: 'HN',
    HK: 'CN', // Hong Kong
    HU: 'HU',
    IS: 'IS',
    IN: 'IN',
    ID: 'ID',
    IR: 'IR',
    IQ: 'IQ',
    IE: 'IE',
    IM: 'GB', // Isle of Man
    IL: 'IL',
    IT: 'IT',
    JM: 'JM',
    JP: 'JP',
    JE: 'GB', // Jersey
    JO: 'JO',
    KZ: 'KZ',
    KE: 'KE',
    KI: 'KI',
    KR: 'KR',
    KW: 'KW',
    KG: 'KG',
    LA: 'LA',
    LV: 'LV',
    LB: 'LB',
    LS: 'LS',
    LR: 'LR',
    LY: 'LY',
    LI: 'LI',
    LT: 'LT',
    LU: 'LU',
    MO: 'CN', // Macao
    MK: 'MK',
    MG: 'MG',
    MW: 'MW',
    MY: 'MY',
    MV: 'MV',
    ML: 'ML',
    MT: 'MT',
    MH: 'MH',
    MQ: 'FR', // Martinique
    MR: 'MR',
    MU: 'MU',
    YT: 'FR', // Mayotte
    MX: 'MX',
    FM: 'FM',
    MD: 'MD',
    MC: 'MC',
    MN: 'MN',
    ME: 'ME',
    MS: 'GB', // Montserrat
    MA: 'MA',
    MZ: 'MZ',
    MM: 'MM',
    NA: 'NA',
    NR: 'NR',
    NP: 'NP',
    NL: 'NL',
    AN: 'NL', // Netherlands Antilles
    NC: 'FR', // New Caledonia
    NZ: 'NZ',
    NI: 'NI',
    NE: 'NE',
    NG: 'NG',
    NU: 'NZ', // Niue
    NF: 'AU', // Norfolk Island
    MP: 'US', // Northern Mariana Islands
    NO: 'NO',
    OM: 'OM',
    PK: 'PK',
    PW: 'PW',
    PS: 'IL', // Palestine
    PA: 'PA',
    PG: 'PG',
    PY: 'PY',
    PE: 'PE',
    PH: 'PH',
    PN: 'GB', // Pitcairn
    PL: 'PL',
    PT: 'PT',
    PR: 'US', // Puerto Rico
    QA: 'QA',
    RE: 'FR', // Reunion
    RO: 'RO',
    RU: 'RU',
    RW: 'RW',
    BL: 'FR', // Saint Barthelemy
    SH: 'GB', // Saint Helena
    KN: 'KN',
    LC: 'LC',
    MF: 'FR', // Saint Martin
    PM: 'FR', // Saint Pierre and Miquelon
    VC: 'VC',
    WS: 'WS',
    SM: 'SM',
    ST: 'ST',
    SA: 'SA',
    SN: 'SN',
    RS: 'RS',
    SC: 'SC',
    SL: 'SL',
    SG: 'SG',
    SX: 'NL', // Sint Maarten
    SK: 'SK',
    SI: 'SI',
    SB: 'SB',
    SO: 'SO',
    ZA: 'ZA',
    GS: 'GB', // South Georgia and the South Sandwich Islands
    ES: 'ES',
    LK: 'LK',
    SD: 'SD',
    SR: 'SR',
    SJ: 'NO', // Svalbard and Jan Mayen
    SZ: 'SZ',
    SE: 'SE',
    CH: 'CH',
    SY: 'SY',
    TW: 'TW', // Taiwan
    TJ: 'TJ',
    TZ: 'TZ',
    TH: 'TH',
    TL: 'TL',
    TG: 'TG',
    TK: 'NZ', // Tokelau
    TO: 'TO',
    TT: 'TT',
    TN: 'TN',
    TR: 'TR',
    TM: 'TM',
    TC: 'GB', // Turcs and Caicos Islands
    TV: 'TV',
    UG: 'UG',
    UA: 'UA',
    AE: 'AE',
    GB: 'GB',
    US: 'US',
    UM: 'US', // US Minor Outlying Islands
    UY: 'UY',
    UZ: 'UZ',
    VU: 'VU',
    VE: 'VE',
    VN: 'VN',
    VG: 'GB', // British Virgin Islands
    VI: 'US', // US Virgin Islands
    WF: 'FR', // Wallis and Futuna
    EH: 'MA', // Western Sahara
    YE: 'YE',
    ZM: 'ZM',
    ZW: 'ZW'
};