I would perform both a left and right single-sided offset buffer on one of the linestrings and test if the other linestring crosses both sides of the offset buffer. You would need to use a suitable small buffer distance for your analysis.
For example, using Shapely:
from shapely.wkt import loads
def actually_crosses(A, B, precis=0.0001):
"""A hybrid spatial predicate that determines if two geometries cross on both sides"""
return (B.crosses(A) and
B.crosses(A.parallel_offset(precis, 'left')) and
B.crosses(A.parallel_offset(precis, 'right')))
# Geometry A: a horizontal line
A = loads('LINESTRING (100 200, 300 200)')
# Geometry Bt: a line that only touches A, but does not actually cross A
Bt = loads('LINESTRING (100 300, 200 200, 300 300)')
# Geometry Bc: a line that actually crosses A
Bc = loads('LINESTRING (100 300, 300 100)')
actually_crosses(A, Bt) # False
actually_crosses(A, Bc) # True
# JTS Validation Suite: Run 4: Test RelateLL
# Case 33: L/L.2-4-4: two LineStrings crossing on one side
A = loads('LINESTRING (60 110, 150 110, 200 160, 250 110, 360 110, 360 210)')
B = loads('LINESTRING (60 110, 110 160, 250 160, 310 160, 360 210)')
A.crosses(B) # True
actually_crosses(A, B) # False
# Case 34: L/L.2-4-5: two LineStrings crossing at two points
A = loads('LINESTRING (130 160, 160 110, 220 110, 250 160, 250 210)')
B = loads('LINESTRING (60 110, 110 160, 250 160, 310 160, 360 210)')
A.crosses(B) # True
actually_crosses(A, B) # True
A similar actually_crosses function could be whipped up in C++ for GEOS or Java for JTS.