Tutorial 5 min read
24

How to extract Apple Maps POI icons symbols

Viktor Shchelochkov
Viktor Shchelochkov Fullstack developer

Today I was working on a virtual map of my Minecraft server, Demovio and wanted to added icons of every building we've made with my friends so far. Apple is always best with design stuff so I wanted to grab icons from there.

Screenshot of Apple Maps app with POI icons in New York

It turned it to be much harder than I thought. Obviously these icons are not present in SF Symbols, they were created especially for maps. Also you can't just pull these from .app's resources.

Finder macOS app showing Maps app files

And they're not in Assets.car...

List of png images with names such as "HikingWelcome" and "MacNotificationWarning"
Decompiled using https://github.com/bartoszj/acextract

So I quickly hooked up mitmproxy and started wandering around maps hoping they'd be downloaded from Apple's servers...

Screenshot of mitmproxy app running in console, intercepting requests on my computer to the internet

After some digging, still nothing. The app actually does download some icons packed into .icondatapack format. I even found them on my computer, cached in ~/Library/Caches/GeoServices/RegionalResources:

List of files with names such as "New_York_City_Icons.icondatapack"

And I even found a way of decompiling them. There is exactly one app in the internet that is capable of doing so, and it's not even the main feature of the app. It's paid and it's fairly easy to find it so I won't link it here, but even after decompressing these Apple Maps icondatapacks I found some brand logos and local city icons, not POI icons.

List of icons with name "Corporate_Icons.png"
Corporate_Icons-43.icondatapack opened

I didn't give up. Obviously there was just one place left where I haven't looked yet, and that's for a reason. Who wants to decompile native Swift apps? Well, Apple didn't give me a choice.

I tried malimite with Ghidra IDE but it looks like it only works for IPA files. So I downloaded Hopper Disassembler paid closed-source software and loaded Apple Maps macOS app there.

Decompiled Maps app through Hopper Disassembler tool

It does let you search through app strings, labels and swift processes, but Apple obviously obfuscates everything. I tried different combinations of search such as "Icon", "Glyph", tried some icons names such as "Pizza" but no luck. I guess they're using UIBezierPath because I found one match for it, but I have no idea where paths are stored in all this mess. I spent a few hours but gave up since no one paid me to spend so much time digging some icons lol.

I had one last trick up my sleeve though. I saved it for last resort but there was no choice — we're digging to web version of Apple Maps. The downside is that these icons have no gradient, but you can fix it yourself easily. The most important thing is that the icons themselves are same and colors seems to match too.

Apple Maps embedded into DuckDuckGo search showing "New York, NY"
https://duckduckgo.com/?q=new+york

Web Apple Maps suck — they're slow, unoptimized and sometimes just reload the page randomly (Apple should definetely hire me — cv.hloth.dev), but somewhere on their CDN they store these icons and it's infinitely times easier to get those through DevTools than through swift decompilation tools.

DevTools showing requests to Apple Maps

So after some reverse engineering and experimenting with mapkit APIs, the workflow for scaping is actually very simple:

  1. Make GET request to https://duckduckgo.com/local.js?get_mk_token=1 (or obtain mapkit token somehow else)
  2. Make GET request to https://cdn.apple-mapkit.com/ma/bootstrap?apiVersion=2&mkjsVersion=5.78.158&poi=1 with Authorization header having token from previous step. This should give you JSON response with "accessKey" in it, which is the key we're hunting for. This will give us access to Apple MapKit CDN
  3. Finally you can make GET requests to https://cdn.apple-mapkit.com with this accessKey in query

Access key looks like this: 1743879209_3285637747132202088_/_gViGKMuzZNzmDskrn2ovwl0pl9XMsGcon7VsPG9kfmg= and works for 30 minutes.

All query parameters for cdn.apple-mapkit.com are very well documented, so you won't have any troubles making those.

I'm just kidding lol. Obviously it's obfuscated too and I have no clue what they mean, but I managed to find one of parameters that gives you different icons and using that I enumerated about 490 different icons.

Basically the URL is:

https://cdn.apple-mapkit.com/md/v1/icon?scale=2&subtype=poi&tint=dark&emphasis=standard&style=1&zoom=13&attributes=4:226,5:3,6:115,10:14,16:1,47:50,82:5,85:11,89:6,164:1,193:1&accessKey=1743875780_4578807989659320900_%2F_sfsG0vv9WUfbnNCBtCA210EMB0WHnPggaB1dEJOS1XM%3D

You should use these parameters:

  • scale — set to 3
  • subtype — set to poi
  • tint — set to dark
  • emphasis — set to standard
  • style — set to 1
  • zoom — set to something very big, like 128
  • attributes — that's the most tricky part, see below
  • accessKey — url encode your access key and put it here

I used 4:226,5:3,6:SOMERANDOMNUMBER,10:0,82:3,85:13,89:1,164:1,193:1 string for attributes and I couldn't find what each of these parameters mean. I only found that they're called "style attributes" in MapKit JavaScript library and these are key-value pairs of something. Replace SOMERANDOMNUMBER with number from 0 to 500 and you should get 52x52 png image.

Here is a JS program I wrote to automate it:

import fs from "fs/promises"

const accessKey = '' // <-- paste your access key here

const max = 500
for (let i = 0; i < max; i++) {
  const scale = 3
  const id = i
  const imgReq = await fetch('https://cdn.apple-mapkit.com/md/v1/icon?scale=' + scale + '&subtype=poi&tint=dark&emphasis=standard&style=1&zoom=128&attributes=4:226,5:3,6:' + id + ',10:0,82:3,85:13,89:1,164:1,193:1&accessKey=' + encodeURIComponent(accessKey));
  if(imgReq.status !== 200) {
    console.log(await imgReq.text());
    console.log(`Error: ${imgReq.status}`);
    console.log(i)
    break
  }
  const content = Buffer.from(await imgReq.arrayBuffer())
  const hash = Bun.hash(content)
  const emptyIconHash = '14793898150699695213'
  if (hash.toString() !== emptyIconHash) {
    await fs.writeFile(`./icons/52x52/${id}.png`, content);
  }
  console.log(id, Math.round(i / max * 100) + '%')
}

// Run `mkdir ./icons && mkdir ./icons/52x52`
// Run with `bun apple-maps-poi-icons-extract.ts`

// This will skip empty icons

The result #

If you just want to grab the icons, go ahead and download the zip file I ended up with after downloading all icons I could get from the CDN.

(you just need to click the "donate" button to reveal content)

Attached files & links
Published at:
Updated at:
0
0
0
0
0
0
0
0
0
0
0
0