Why most shop QR codes fail: the engineering behind location QR codes
A shop puts a QR code on the counter. A customer scans it. Nothing happens. Or something happens, but it's a Google Maps search box pre-filled with the text "123 Main St, Springfield" — and the customer has to tap "searc
A shop puts a QR code on the counter. A customer scans it. Nothing happens. Or something happens, but it's a Google Maps search box pre-filled with the text "123 Main St, Springfield" — and the customer has to tap "search" themselves, on a phone, in a hurry, with one hand holding a bag.
The owner almost never finds out. They tested it once, on their own phone, in their own scanner app, at arm's length, under the shop lights. Four variables, and three of them were wrong.
A location QR code is one of the simplest QR payloads you can build, and also one of the easiest to get subtly wrong, because the failure is silent: the code scans fine, the phone does something, and nobody complains.
This is a working guide to the parts that actually decide whether it works in the real world: the payload, the QR version, the module pitch, the print substrate, and the test protocol. Nothing here requires a paid service.
- The payload decides everything that follows A QR code is a container. A "location QR code" is just a container holding a string that a phone can turn into a map view. There are four realistic strings, and they behave very differently. Payload Example Android iOS Failure mode geo: URI geo:37.4224764,-122.0842499 Opens default maps app Generally unhandled — nothing happens Dead on half your customers Google Maps universal link https://www.google.com/maps?q=37.4224764,-122.0842499 Google Maps app if installed, else web Google Maps app if installed, else web None material Apple Maps link https://maps.apple.com/?q=37.4224764,-122.0842499 Opens a web page Apple Maps Looks broken on Android Plus Code (Open Location Code) 8FVC9G8F+6W Shows text, user must copy and paste Shows text, user must copy and paste Requires two more taps
The short version: use an HTTPS maps link, not geo:. geo: is the "correct" URI scheme in the spec sense and the worst choice in practice, because iOS does not hand it to Apple Maps. You will test it on Android, it will work, and it will fail for roughly half of your customers.
There is a second decision nested inside the payload: coordinates or a text address?
A text address (...?api=1&query=123+Main+St) is friendly to read but brittle — it depends on the maps geocoder resolving that string, in that country, in that language. A mall unit number, a new building, or a non-Latin address will geocode badly or land on the wrong block. Coordinates are unambiguous, and they cost you nothing.
Also relevant: precision. Six decimal places is about 11 cm. Seven is noise. Writing 37.4224764 when 37.4225 would do is pure payload bloat, and payload bytes translate directly into a denser QR code (more on that in the next section). Truncate to 5–6 decimals and stop.
If your location genuinely has no usable address — a trailhead, a market stall, a warehouse bay — the Plus Code is the right answer, because it resolves offline and does not depend on any geocoder. It just requires one extra user action, so it is a fallback, not a default.
One more payload trap: non-ASCII characters. A Chinese, Japanese or Arabic address inside a URL has to be percent-encoded, and every encoded character becomes three bytes. If you are building payloads from user input, encode the query component and prefer coordinates when the address is non-Latin.
- Payload length → version → module size (the math nobody does) QR codes come in versions 1 through 40. The rule is simple: modules per side = 17 + 4 × version
So version 1 is 21×21 modules, version 4 is 33×33, version 10 is 57×57, version 40 is 177×177.
An encoder picks the smallest version that fits your payload at your chosen error-correction level. Byte-mode capacity at level M is roughly: 14 bytes at v1, 26 at v2, 42 at v3, 62 at v4, 106 at v6, 213 at v10.
Now the thing that matters: the QR code's printed width is fixed by your design, so version and module size trade off against each other.
module pitch (mm) = printed width (mm) / modules per side
Take a 50 mm counter sticker — a typical, reasonable choice.
Payload
Length
Version
Modules
Module pitch at 50 mm
Short maps URL, coordinates
54 chars
4
33×33
1.52 mm
Maps URL with a text address
75 chars
5
37×37
1.35 mm
Maps URL + a tracking parameter
120 chars
7
45×45
1.11 mm
geo: URI, same precision
28 chars
3
29×29
1.72 mm
That last row is the whole argument in miniature: the geo: version is denser-looking but physically coarser, because it is shorter. Fewer characters means fewer modules means fatter modules at the same print size means it scans from further away, in worse light, with a cheaper phone. Every character you add costs you scan robustness.
This is also why "just add a utm_source and an ID and a timestamp" wrecks a sticker that used to work.
Practical consequences:
Keep the payload under ~60 characters if the code will be printed small (under 60 mm).
Do not append analytics parameters to a printed code. If you need scan counts, see section 7.
Print from vector output (SVG) whenever possible. A QR code is geometry; letting a print shop resample a 300 px PNG is how you get soft module edges.
If you must ship a raster: at 0.9 mm module pitch you need at least 2 px per module, so a 50 mm/49-module code needs ~98 px across — but give the printer 1000+ px and let them downscale. Never upscale.
- Scan distance is a geometric problem Two constraints, both real: Module pitch vs camera resolution. At close range a phone can resolve a ~0.3–0.4 mm module on matte paper under decent light. Below that, decode fails on older cameras. Field of view vs distance. A rough industry rule: the usable scan distance is about 10× the code's physical width. A 50 mm code wants the phone within ~50 cm. A 300 mm poster code wants ~3 m. Combine them when you design the placement rather than the code: required width (mm) ≈ scan distance (mm) / 10 required module pitch (mm) = required width / modules per side
If a QR code has to be read from 3 m (a poster in a window, seen from the street), with a 33-module code:
width = 3000 / 10 = 300 mm
pitch = 300 / 33 = 9.1 mm
A 30 cm QR code for a 3 m read. That feels absurdly large to a designer and is exactly right for a doorway. Conversely, a 40 mm table-tent card read from 25 cm only needs a 0.75 mm pitch — achievable, but only if the payload stays short enough to keep the version low.
If you want a single number to remember: a 50 mm sticker with a short HTTPS payload is the sweet spot for counter and tabletop use. Below 40 mm, stop optimizing the design and start shortening the payload.
Error correction: the advice you'll read everywhere is half wrong
QR error correction levels recover approximate fractions of codewords: L ≈ 7%, M ≈ 15%, Q ≈ 25%, H ≈ 30%.
The common recommendation — "always use H, it's the most robust" — is wrong for printed stickers, and wrong for a mechanical reason: higher error correction means more data has to fit in the same code, which pushes the version up, which shrinks the modules at your fixed print size. You have traded real, physical module size for an abstract recovery budget you probably don't need.
Use H only when something is physically eating the code:
A logo or graphic is overlaid. Keep it under ~30% of the area if you use H. Under 15% with Q.
The code is on a curved, dirty or partially-covered surface (a table edge, a crate, a machine panel).
The code will be behind scratched acrylic, or outdoors.
Otherwise, M with maximum physical size beats H with a denser grid nearly every time. A clean 33-module code at 1.5 mm pitch will out-scan a 45-module code at 1.1 mm pitch in every real-world condition, including the one you care about: a phone at an angle, in a lit shop, by someone who is not trying very hard.
One caveat where H genuinely does earn its keep: the code is going to get damaged over its life — stickers on a counter get scratched, sprayed, and picked at. If the code must survive 3 years of that, H plus a laminating pocket is defensible. Just measure the resulting module size before you commit.Print mechanics that quietly break scanning
These are not QR-spec issues. They are the reasons a technically perfect file scans badly on a counter.
Quiet zone. The spec asks for 4 modules of empty margin. Brand templates trim it to look tight, and trimming it is one of the most common causes of "it works for me but not for customers." Whatever margin your generator emits, keep at least 4 modules, and more if the code sits on a busy background.
Contrast, not color. Dark modules on light background, high luminance contrast. Avoid mid-tone pairs (navy on teal scans badly at small sizes) and avoid metallic foils — gold on black has almost no luminance contrast despite looking like contrast.
Inversion. A white code on a black sticker looks great and decodes unreliably; many scanners assume dark-on-light. If you invert, test on the oldest phone you can find.
Specular highlights. Gloss laminate plus a ceiling spotlight equals a blown-out patch — exactly where the finder patterns are, if you're unlucky. Matte laminate or uncoated stock is the cheap fix.
Refraction. A code printed on paper and placed behind glass or acrylic gets a double image at an angle. Print on the outside face, or use a matte film on the inside.
Placement geometry. Roughly 1.2–1.5 m above floor, flat to the viewer, ideally not directly under a downlight. A code stuck flat on a horizontal counter is scanned at a steep angle — a small angled stand fixes more real-world failures than any encoder setting.Building one in the browser (no server, no account)
A location QR code needs no backend. The payload is a string, the encoder is a library, and the output is geometry.
// payload: coordinates win over text addresses
const payload =https://www.google.com/maps?q=${lat.toFixed(6)},${lng.toFixed(6)};
// 0 = auto version, 'M' = error correction level
const qr = qrcode(0, 'M');
qr.addData(payload, 'Byte');
qr.make();
// margin: 4 -> the 4-module quiet zone; scalable -> vector output
document.getElementById('out').innerHTML =
qr.createSvgTag({ scalable: true, margin: 4 });
Four things to notice in those seven lines, because each one is a real decision:
toFixed(6) — six decimals is centimetre accuracy. More is payload bloat.
0 for version — let the encoder pick the smallest that fits. Hard-coding a version is how you end up with a code that stops scanning when someone adds a character to the address.
'M', not 'H' — see section 4.
margin: 4 and scalable: true — the quiet zone travels with the file, and the print shop gets vectors instead of pixels.
If you serve this as a page, generate client-side and never round-trip the coordinates through your server: it's a single string, and a client-side generator means the tool keeps working when your hosting bill doesn't.
Static vs dynamic: pick based on the sticker's lifespan
Static (payload embedded in the code): works forever, works offline, needs no domain, cannot be revoked by anyone. Scans cannot be counted.
Dynamic (short URL that redirects): destination is editable, scans are measurable — and the code dies the day the domain, the subscription, or the redirect service dies.
A printed sticker on a shop counter lives 3–5 years. A free short-link service does not reliably live 3–5 years, and when it dies the shop has a dead square on its counter and no idea why. If you need the measurements, host the redirect yourself on a domain you own, with a static fallback: the redirect page should show the address and a maps link as plain text, so a dead redirect degrades to a usable page instead of a 404.
For a small business, static plus a self-hosted one-line redirect behind your own domain is the only combination that survives contact with reality.Test protocol (the part everyone skips)
Print one sheet with three sizes — 40 mm, 60 mm, 100 mm — before you commit to a run of stickers. Then check, in this order:
Three phones: one iPhone, one Android, one at least four years old.
Two readers: the stock camera app, and one third-party scanner. The stock camera is the one your customers use; third-party apps are more forgiving and will hide your problems.
Expected distance, not arm's length. Scan a counter sticker from where a customer actually stands.
Actual lighting. Daylight, evening, and directly under the shop's downlight.
After lamination, if you're laminating.
After a week of dust and a thumbprint. This is not paranoia; counter stickers get touched.
Failure symptoms map to causes with almost boring regularity:
Symptom
Cause
Nothing happens at all
Code doesn't decode: module pitch too small, quiet zone trimmed, or contrast too low
Opens a maps search box with text in it
Payload was a text address, not coordinates or a maps URL
Opens a web page instead of the app
No universal-link handler, or the app isn't installed
Pin lands in the ocean
Latitude and longitude swapped — the classic
Opens zoomed out to city level
No explicit zoom on the maps link
Works on your phone, not on customers'
You tested with a lenient scanner app, at arm's length
The one I'd underline: swapped lat/lng. It produces a map that opens, looks plausible, and is wrong by thousands of kilometres. Your customers see it, assume the code is broken, and leave. Validate coordinates with a range check before generating: latitude −90 to 90, longitude −180 to 180.
- A size table to work from Placement Printed width Target payload Max version Module pitch Counter sticker / table tent 40–60 mm < 60 chars 4 (33×33) ≥ 1.2 mm Window decal (read outside) 120–150 mm < 80 chars 6 (41×41) ≥ 2.5 mm Menu card, tabletop stand 40–50 mm ≤ 42 chars 3 (29×29) ≥ 1.3 mm Poster / street-facing sign 250–300 mm any ≤ 10 ≥ 5 mm Vehicle, moving target 300–400 mm any ≤ 8 ≥ 9 mm
What this adds up to
Almost every location QR code failure I've looked at reduces to one of four causes, none of which is the encoder's fault:
The payload was the wrong type (geo: on iOS, or a text address instead of coordinates).
The payload was too long, so the code was too dense for its print size.
The print violated quiet zone, contrast, or flatness.
Nobody tested at the right distance, in the right light, on the right phone.
Fix those and the code works — not "works when I try it", but works for a stranger in a hurry holding a bag.
I build a small set of browser-side QR tools, and this one came out of exactly the failures above: enter an address or a pair of coordinates, get a vector PNG/SVG back, no account, no tracking, no redirect service in the middle. If it's useful, it's here: Location QR Code Generator. The payload logic in section 6 is the whole tool, so you can also just build your own — that's the more durable option.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.